diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 000000000..6a109e171 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,5 @@ +[profile.ci] +fail-fast = false + +[profile.ci.junit] +path = "junit.xml" diff --git a/.github/ISSUE_TEMPLATE/01-feature.yml b/.github/ISSUE_TEMPLATE/01-feature.yml new file mode 100644 index 000000000..210af979e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-feature.yml @@ -0,0 +1,30 @@ +name: 💡 Feature Request +description: Propose new functionality for the SDK +labels: ["Rust", "Feature"] +body: + - type: markdown + attributes: + value: Thanks for taking the time to file a feature request! Please fill out this form as completely as possible. + - type: textarea + id: problem + attributes: + label: Problem Statement + description: A clear and concise description of what you want and what your use case is. + placeholder: |- + I want to make whirled peas, but Sentry doesn't blend. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Solution Brainstorm + description: We know you have bright ideas to share ... share away, friend. + placeholder: |- + Add a blender to Sentry. + validations: + required: true + - type: markdown + attributes: + value: |- + ## Thanks 🙏 + Check our [triage docs](https://open.sentry.io/triage/) for what to expect next. diff --git a/.github/ISSUE_TEMPLATE/02-improvement.yml b/.github/ISSUE_TEMPLATE/02-improvement.yml new file mode 100644 index 000000000..3e4ac56c0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-improvement.yml @@ -0,0 +1,30 @@ +name: 💡 Improvement +description: Propose an improvement for existing functionality of the SDK +labels: ["Rust", "Improvement"] +body: + - type: markdown + attributes: + value: Thanks for taking the time to file a request! Please fill out this form as completely as possible. + - type: textarea + id: problem + attributes: + label: Problem Statement + description: A clear and concise description of what you want and what your use case is. + placeholder: |- + I want to make whirled peas, but Sentry doesn't blend. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Solution Brainstorm + description: We know you have bright ideas to share ... share away, friend. + placeholder: |- + Add a blender to Sentry. + validations: + required: true + - type: markdown + attributes: + value: |- + ## Thanks 🙏 + Check our [triage docs](https://open.sentry.io/triage/) for what to expect next. diff --git a/.github/ISSUE_TEMPLATE/03-bug.yml b/.github/ISSUE_TEMPLATE/03-bug.yml new file mode 100644 index 000000000..b9ba538dc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03-bug.yml @@ -0,0 +1,51 @@ +name: 🐞 Bug Report +description: Tell us about something that's not working the way we (probably) intend. +labels: ["Rust", "Bug"] +body: + - type: dropdown + id: type + attributes: + label: How do you use Sentry? + options: + - Sentry SaaS (sentry.io) + - Self-hosted / on-premises + validations: + required: true + - type: input + id: version + attributes: + label: SDK version + description: Which SDK version do you use? + placeholder: e.g. 4.9.2 + validations: + required: true + - type: textarea + id: repro + attributes: + label: Steps to reproduce + description: How can we see what you're seeing? Specific is terrific. + placeholder: |- + 1. What + 2. you + 3. did. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected result + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual result + description: Logs? Screenshots? Yes, please. + validations: + required: true + - type: markdown + attributes: + value: |- + ## Thanks 🙏 + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/04-blank.yml b/.github/ISSUE_TEMPLATE/04-blank.yml new file mode 100644 index 000000000..544fe60f5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/04-blank.yml @@ -0,0 +1,11 @@ +name: Blank Issue +description: Blank Issue. Reserved for maintainers. +labels: ["Rust"] +body: + - type: textarea + id: description + attributes: + label: Description + description: Please describe the issue. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..ec915930a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,9 @@ +blank_issues_enabled: false +contact_links: + - name: Join Sentry Discord + url: https://discord.com/invite/sentry + about: A place to talk about SDK development and other Sentry related topics. It's not meant as a support channel. + - name: Support Request + url: https://sentry.io/support + about: Use our dedicated support channel for paid accounts. + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..bedde672b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,14 @@ +### Description + + +#### Issues + + +#### Reminders +- Add GH Issue ID _&_ Linear ID (if applicable) +- Add an entry to CHANGELOG.md, following the format of existing entries +- The PR title should use [Conventional Commits](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`, etc.) +- Useful links for external contributors: [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/sentry) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f34e0ff8..87f0dd07c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - run: rustup component add rustfmt clippy @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 @@ -50,29 +50,40 @@ jobs: name: Test using Rust stable on ${{ matrix.os }} runs-on: ${{ matrix.os }} + permissions: + id-token: write # required by `getsentry/prevent-action` steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - - name: Run cargo test - run: cargo test --workspace --all-features --all-targets + - uses: taiki-e/install-action@nextest + + - name: Run tests with nextest + run: cargo nextest run --profile ci --all-features --all-targets + + - name: Upload test results to Sentry Prevent + if: ${{ !cancelled() }} + uses: getsentry/prevent-action@v0 + with: + files: target/nextest/ci/junit.xml + disable_search: true MSRV: strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - rust: [1.73.0] + rust: [1.81.0] name: Check / Test MSRV on ${{ matrix.os }} runs-on: ${{ matrix.os }} steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install rust ${{ matrix.rust }} toolchain run: | @@ -90,7 +101,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: rustup component add llvm-tools-preview @@ -100,9 +111,10 @@ jobs: - run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info - - uses: codecov/codecov-action@e156083f13aff6830c92fc5faa23505779fbf649 + - uses: codecov/codecov-action@v5 with: files: lcov.info + token: ${{ secrets.CODECOV_TOKEN }} doc: name: Build-test documentation @@ -112,7 +124,7 @@ jobs: steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - run: rustup component add rust-docs diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml new file mode 100644 index 000000000..09d4bcb03 --- /dev/null +++ b/.github/workflows/danger.yml @@ -0,0 +1,11 @@ +name: Danger + +on: + pull_request: + types: [opened, synchronize, reopened, edited, ready_for_review] + +jobs: + danger: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/danger@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bde0e7b5a..1f711e048 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,9 +15,16 @@ jobs: runs-on: ubuntu-latest name: "Release a new version" steps: - - uses: actions/checkout@v3 + - name: Get auth token + id: token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 with: - token: ${{ secrets.GH_RELEASE_PAT }} + app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} + private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} + + - uses: actions/checkout@v4 + with: + token: ${{ steps.token.outputs.token }} fetch-depth: 0 - run: cargo install cargo-readme @@ -25,7 +32,7 @@ jobs: - name: Prepare release uses: getsentry/action-prepare-release@v1 env: - GITHUB_TOKEN: ${{ secrets.GH_RELEASE_PAT }} + GITHUB_TOKEN: ${{ steps.token.outputs.token }} with: version: ${{ github.event.inputs.version }} force: ${{ github.event.inputs.force }} diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index 33f074294..4e37ca838 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -19,7 +19,7 @@ jobs: if: github.repository_owner == 'getsentry' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install rust stable toolchain run: | @@ -36,9 +36,8 @@ jobs: if: github.repository_owner == 'getsentry' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - # FIXME: find a maintained alternative to audit-check - - uses: actions-rs/audit-check@v1 + - uses: rustsec/audit-check@69366f33c96575abad1ee0dba8212993eecbe998 # v2.0.0 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 23fd35f0e..bd1e33d73 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "editor.formatOnSave": true + "editor.formatOnSave": true, + "rust-analyzer.cargo.features": "all" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 45483f584..71621ac13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,384 @@ ## Unreleased +### Fixes + +- fix: adjust sentry.origin for log integration ([#919](https://github.com/getsentry/sentry-rust/pull/919)) by @lcian + +## 0.45.0 + +### Breaking changes + +- Add custom variant to `AttachmentType` that holds an arbitrary String. ([#916](https://github.com/getsentry/sentry-rust/pull/916)) + +## 0.44.0 + +### Breaking changes + +- feat(log): support combined LogFilters and RecordMappings ([#914](https://github.com/getsentry/sentry-rust/pull/914)) by @lcian + - Breaking change: `sentry::integrations::log::LogFilter` has been changed to a `bitflags` struct. + - It's now possible to map a `log` record to multiple items in Sentry by combining multiple log filters in the filter, e.g. `log::Level::ERROR => LogFilter::Event | LogFilter::Log`. + - If using a custom `mapper` instead, it's possible to return a `Vec` to map a `log` record to multiple items in Sentry. + +### Behavioral changes + +- ref(log): send logs by default when logs feature flag is enabled ([#915](https://github.com/getsentry/sentry-rust/pull/915)) by @lcian + - If the `logs` feature flag is enabled, the default Sentry `log` logger now sends logs for all events at or above INFO. +- ref(logs): enable logs by default if logs feature flag is used ([#910](https://github.com/getsentry/sentry-rust/pull/910)) by @lcian + - This changes the default value of `sentry::ClientOptions::enable_logs` to `true`. + - This simplifies the setup of Sentry structured logs by requiring users to just add the `log` feature flag to the `sentry` dependency to opt-in to sending logs. + - When the `log` feature flag is enabled, the `tracing` and `log` integrations will send structured logs to Sentry for all logs/events at or above INFO level by default. + +## 0.43.0 + +### Breaking changes + +- ref(tracing): rework tracing to Sentry span name/op conversion ([#887](https://github.com/getsentry/sentry-rust/pull/887)) by @lcian + - The `tracing` integration now uses the tracing span name as the Sentry span name by default. + - Before this change, the span name would be set based on the `tracing` span target (`::` when using the `tracing::instrument` macro). + - The `tracing` integration now uses `::` as the default Sentry span op (i.e. `::` when using `tracing::instrument`). + - Before this change, the span op would be set based on the `tracing` span name. + - Read below to learn how to customize the span name and op. + - When upgrading, please ensure to adapt any queries, metrics or dashboards to use the new span names/ops. +- ref(tracing): use standard code attributes ([#899](https://github.com/getsentry/sentry-rust/pull/899)) by @lcian + - Logs now carry the attributes `code.module.name`, `code.file.path` and `code.line.number` standardized in OTEL to surface the respective information, in contrast with the previously sent `tracing.module_path`, `tracing.file` and `tracing.line`. +- fix(actix): capture only server errors ([#877](https://github.com/getsentry/sentry-rust/pull/877)) by @lcian + - The Actix integration now properly honors the `capture_server_errors` option (enabled by default), capturing errors returned by middleware only if they are server errors (HTTP status code 5xx). + - Previously, if a middleware were to process the request after the Sentry middleware and return an error, our middleware would always capture it and send it to Sentry, regardless if it was a client, server or some other kind of error. + - With this change, we capture errors returned by middleware only if those errors can be classified as server errors. + - There is no change in behavior when it comes to errors returned by services, in which case the Sentry middleware only captures server errors exclusively. +- fix: send trace origin correctly ([#906](https://github.com/getsentry/sentry-rust/pull/906)) by @lcian + - `TraceContext` now has an additional field `origin`, used to report which integration created a transaction. + +### Behavioral changes + +- feat(tracing): send both breadcrumbs and logs by default ([#878](https://github.com/getsentry/sentry-rust/pull/878)) by @lcian + - If the `logs` feature flag is enabled, and `enable_logs: true` is set on your client options, the default Sentry `tracing` layer now sends logs for all events at or above INFO. + +### Features + +- ref(tracing): rework tracing to Sentry span name/op conversion ([#887](https://github.com/getsentry/sentry-rust/pull/887)) by @lcian + - Additional special fields have been added that allow overriding certain data on the Sentry span: + - `sentry.op`: override the Sentry span op. + - `sentry.name`: override the Sentry span name. + - `sentry.trace`: given a string matching a valid `sentry-trace` header (sent automatically by client SDKs), continues the distributed trace instead of starting a new one. If the value is not a valid `sentry-trace` header or a trace is already started, this value is ignored. + - `sentry.op` and `sentry.name` can also be applied retroactively by declaring fields with value `tracing::field::Empty` and then recorded using `tracing::Span::record`. + - Example usage: + ```rust + #[tracing::instrument(skip_all, fields( + sentry.op = "http.server", + sentry.name = "GET /payments", + sentry.trace = headers.get("sentry-trace").unwrap_or(&"".to_owned()), + ))] + async fn handle_request(headers: std::collections::HashMap) { + // ... + } + ``` + - Additional attributes are sent along with each span by default: + - `sentry.tracing.target`: corresponds to the `tracing` span's `metadata.target()` + - `code.module.name`, `code.file.path`, `code.line.number` + +- feat(core): add Response context ([#874](https://github.com/getsentry/sentry-rust/pull/874)) by @lcian + - The `Response` context can now be attached to events, to include information about HTTP responses such as headers, cookies and status code. + - Example: + ```rust + let mut event = Event::new(); + let response = ResponseContext { + cookies: Some(r#""csrftoken": "1234567""#.to_owned()), + headers: Some(headers_map), + status_code: Some(500), + body_size: Some(15), + data: Some("Invalid request"), + }; + event + .contexts + .insert("response".to_owned(), response.into()); + ``` + +### Fixes + +- build(panic): Fix build without other dependencies ([#883](https://github.com/getsentry/sentry-rust/pull/883)) by @liskin + - The `sentry-panic` crate now builds successfully when used as a standalone dependency. +- fix(transport): add rate limits for logs ([#894](https://github.com/getsentry/sentry-rust/pull/894)) by @giortzisg + +## 0.42.0 + +### Features + +- feat(log): support kv feature of log (#851) by @lcian + - Attributes added to a `log` record using the `kv` feature are now recorded as attributes on the log sent to Sentry. +- feat(types): add all the missing supported envelope headers ([#867](https://github.com/getsentry/sentry-rust/pull/867)) by @lcian +- feat(types): add setters for envelope headers ([#868](https://github.com/getsentry/sentry-rust/pull/868)) by @lcian + - It's now possible to set all of the [envelope headers](https://develop.sentry.dev/sdk/data-model/envelopes/#headers) supported by the protocol when constructing envelopes. +- feat(core): add some DSC fields to transaction envelope headers ([#869](https://github.com/getsentry/sentry-rust/pull/869)) by @lcian + - The SDK now sends additional envelope headers with transactions. This should solve some extrapolation issues for span metrics. + +### Behavioral changes + +- feat: filter username and password in URLs ([#864](https://github.com/getsentry/sentry-rust/pull/864)) by @lcian + - Usernames and passwords that could be contained in URLs captured when using the Actix Web or axum integration are now always filtered out. + - If the `Request` is created manually by the user, then these fields are not filtered out. + - This information was already filtered by Relay, but should also be filtered by the SDK itself as a first line of defense. + +### Fixes + +- docs: match description of `debug` option with behavior since PR #820 ([#860](https://github.com/getsentry/sentry-rust/pull/860)) by @AlexTMjugador + +## 0.41.0 + +### Breaking changes + +- feat(tracing): support combined EventFilters and EventMappings (#847) by @lcian + - `EventFilter` has been changed to a `bitflags` struct. + - It's now possible to map a `tracing` event to multiple items in Sentry by combining multiple event filters in the `event_filter`, e.g. `tracing::Level::ERROR => EventFilter::Event | EventFilter::Log`. + - It's also possible to use `EventMapping::Combined` to map a `tracing` event to multiple items in Sentry. + - `ctx` in the signatures of `event_from_event`, `breadcrumb_from_event` and `log_from_event` has been changed to take `impl Into>>` to avoid cloning the `Context` when mapping to multiple items. + +### Features + +- feat(core): emit debug log when calling capture_log but logs are disabled (#849) by @lcian + +### Fixes + +- fix(logs): stringify u64 attributes greater than `i64::MAX` (#846) by @lcian + +### Dependencies + +- chore(deps): bump `anyhow` and disable its `backtrace` feature (#632) by @LunaBorowska + +## 0.40.0 + +### Breaking changes + +- refactor(logs): apply user attributes to log regardless of `send_default_pii` (#843) by @lcian + - User attributes should be applied to logs regardless of `send_default_pii`. Therefore, that parameter was removed from `sentry_core::Scope::apply_to_log`. + +### Features + +- feat(tracing): add support for logs (#840) by @lcian + - To capture `tracing` events as Sentry structured logs, enable the `logs` feature of the `sentry` crate. + - Then, initialize the SDK with `enable_logs: true` in your client options. + - Finally, set up a custom event filter to map events to logs based on criteria such as severity. For example: + ```rust + let sentry_layer = sentry_tracing::layer().event_filter(|md| match *md.level() { + tracing::Level::ERROR => EventFilter::Event, + tracing::Level::TRACE => EventFilter::Ignore, + _ => EventFilter::Log, + }); + ``` +- feat(log): add support for logs (#841) by @lcian + - To capture `log` records as Sentry structured logs, enable the `logs` feature of the `sentry` crate. + - Then, initialize the SDK with `enable_logs: true` in your client options. + - Finally, set up a custom event filter to map records to Sentry logs based on criteria such as severity. For example: + ```rust + let logger = sentry::integrations::log::SentryLogger::new().filter(|md| match md.level() { + log::Level::Error => LogFilter::Event, + log::Level::Trace => LogFilter::Ignore, + _ => LogFilter::Log, + }); + ``` +- refactor(logs): cache default attributes and add OS attributes (#842) by @lcian + - `os.name` and `os.version` are now being attached to logs as default attributes. + +### Fixes + +- fix(logs): send environment in `sentry.environment` default attribute (#837) by @lcian + +### Behavioral changes + +- refactor(tracing): refactor internal code and improve docs (#839) by @lcian + - Errors carried by breadcrumbs will now be stored in the breadcrumb `data` under their original field name. + - Before, they were all stored under a single key called `errors`. + +### Dependencies + +- chore(deps): upgrade `ureq` to 3.x (#835) by @algesten + +## 0.39.0 + +### Features + +Support for [Sentry structured logs](https://docs.sentry.io/product/explore/logs/) has been added to the SDK. +- To set up logs, enable the `logs` feature of the `sentry` crate and set `enable_logs` to `true` in your client options. +- Then, use the `logger_trace!`, `logger_debug!`, `logger_info!`, `logger_warn!`, `logger_error!` and `logger_fatal!` macros to capture logs. +- To filter or update logs before they are sent, you can use the `before_send_log` client option. +- Please note that breaking changes could occur until the API is finalized. + +- feat(logs): add log protocol types (#821) by @lcian +- feat(logs): add ability to capture and send logs (#823) by @lcian & @Swatinem +- feat(logs): add macro-based API (#827) by @lcian & @szokeasaurusrex +- feat(logs): send logs in batches (#831) by @lcian + +### Behavioral changes + +- feat(core): implement Tracing without Performance (#811) by @lcian + - The SDK now implements Tracing without Performance, which makes it so that each `Scope` is associated with an object holding some tracing information. + - This information is used as a fallback when capturing an event with tracing disabled or otherwise no ongoing span, to still allow related events to be linked by a trace. + - A new API `Scope::iter_trace_propagation_headers` has been provided that will use the fallback tracing information if there is no current `Span` on the `Scope`. + +### Breaking changes + +- refactor: remove `debug-logs` feature (#820) by @lcian + - The deprecated `debug-logs` feature of the `sentry` crate, used for the SDK's own internal logging, has been removed. + +## 0.38.1 + +### Fixes + +- build: include `sentry-actix` optionally when `release-health` is enabled (#806) by @lcian + - `sentry-actix` is now being included as a dependency only when explicitly added, either as a direct dependency or through the `actix` feature flag of the `sentry` crate. + - Due to a mistake in the `Cargo.toml`, it was previously being included as a dependency by default when using just the `sentry` crate with default features. + +## 0.38.0 + +### OpenTelemetry integration + +An OpenTelemetry integration has been released. Please refer to the changelog entry below for the details. + +### Breaking changes + +- refactor(tracing): remove `EventFilter::exception` and always attach exception (#768) by @lcian + - The `EventFilter::Exception` enum variant has been removed. Please use `EventFilter::Event` instead to achieve the same behavior. + - Using `EventFilter::Event` will always attach any error struct used within the `error` field passed to the `tracing` macro, as `EventFilter::Exception` did previously. + - The `error` field will also be attached to breadcrumbs as an `errors` field resembling the structure of Sentry events created from error structs. +- fix: use `release-health` flag in `sentry-actix` and remove it from subcrates where unneeded (#787) by @lcian + - As a follow-up from the changes in the previous release, the `ClientOptions` fields `auto_session_tracking` and `session_mode` are now gated behind the `release-health` feature flag of the `sentry` crate. + - If you depend on `sentry` with `default-features = false`, you need to include the `release-health` feature flag to benefit from the [Release Health](https://docs.sentry.io/product/releases/health/) features of Sentry and have access to the aforementioned client options. + - The `release-health` feature flag is used correctly in `sentry-actix` to enable compilation of that subcrate when it's disabled. + - The `release-health` has been removed from the `sentry-tracing` and `sentry-tower` subcrates, where it was unnecessary. +- refactor: remove Surf transport (#766) by @lcian + - The Surf transport has been removed as the `surf` crate is unmaintained and it was holding back dependency upgrades. + - If you really want to still use Surf, you can define a custom `TransportFactory` and pass it as the `transport` in your `ClientOptions` + +### Behavioral changes + +- refactor: honor `send_default_pii` in `sentry-actix` and `sentry-tower` (#771) by @lcian + - The client option `send_default_pii` (disabled by default) is now honored by `sentry-actix` and `sentry-tower`. + - This means that potentially sensitive headers such as authorization, cookies, and those that usually contain the user's IP address are filtered and not sent to Sentry. + - If you want to get back to the previous behavior and capture all headers, please set `send_default_pii` to `true` in your `ClientOptions`. + - Please refer to our [Data Collected](https://docs.sentry.io/platforms/rust/data-management/data-collected/) page for a comprehensive view of the data collected by the SDK. +- refactor(debug-images): force init `DEBUG_META` on integration init (#773) by @lcian + - The `DebugImages` integration has been updated to init the `DEBUG_META` `Lazy` immediately. + - Using this integration is known to cause issues in specific versions of the Linux kernel due to issues in a library it depends on. + - Previously, on problematic systems the SDK would cause deadlock after capturing the first event. Now the SDK will panic on initialization instead. Please open an issue if you're affected. + +### Features + +- feat(otel): add OpenTelemetry SpanProcessor, Propagator, Extractor (#779) by @lcian + - A new integration for the `opentelemetry` crate has been released. + - It can be used to capture spans created using the `opentelemetry` API and send them to Sentry. + - Distributed tracing is also supported, provided that the upstream/downstream services support the Sentry or W3C distributed tracing metadata format. + - Please refer to the subcrate's README or the crate docs to see an example of setup and usage. +- feat: expose `sentry-actix` as a feature of `sentry` (#788) by @lcian + - `sentry-actix` is now exposed by the `sentry` crate as `sentry::integrations::actix`, gated behind the `actix` feature flag. + - Please update your dependencies to not depend on the `sentry-actix` subcrate directly. + +### Dependencies + +- build(deps): bump openssl from 0.10.71 to 0.10.72 (#762) by @dependabot +- build(deps): bump tokio from 1.44.1 to 1.44.2 (#763) by @dependabot +- chore(deps): bump some dependencies and update `Cargo.lock` (#772) by @lcian + +### Various fixes & improvements + +- Replace `once_cell` with `std::sync::LazyLock` (#776) by @FalkWoldmann +- chore: update GH issue templates for Linear compatibility (#777) by @stephanie-anderson +- chore: update issue templates with blank issue and Discord link (#778) by @lcian +- refactor(core): fail with message if TLS backend not available (#784) by @lcian +- build: add `sentry-opentelemetry` to workspace (#789) by @lcian +- docs: update docs including OTEL and other integrations (#790) by @lcian +- fix(otel): fix doctests (#794) by @lcian +- fix(otel): fix span and trace ids for distributed tracing (#801) by @lcian +- build(otel): exclude version from circular dev-dependencies (#802) by @lcian + +## 0.37.0 + +### Breaking changes + +- chore(msrv): `cargo update` and bump MSRV to 1.81 (#754) by @lcian + - The minimum supported Rust version has been raised to 1.81. +- feat(core): introduce `release-health` feature (#749) by @pepperoni505 + - A new `release-health` feature flag was introduced that gates the [Release Health](https://docs.sentry.io/product/releases/health/) features of Sentry. + - This allows for compilation of the SDK on certain WASM targets. + - Release Health features were already present and enabled with no feature flag in previous versions. + - The new feature flag will be enabled by default when using `sentry`, `sentry-actix`, `sentry-tower` or `sentry-tracing` with the default features. + - If you're fine-tuning your feature flags, make sure to enable `release-health` to get back the previous behavior. +- ref(metrics): remove features and code related to the old metrics beta (#740) by @lcian + - The metrics feature and the code related to it has been removed from the crate, as the Sentry backend stopped ingesting metrics a while ago. +- Switch to MIT license (#724) by @cleptric + - The license for the crates has been changed to MIT. + +### Features + +- feat(actix): capture HTTP request body (#731) by @pacifistes + - The middleware for `actix-web` now supports capturing and attaching the request body to HTTP request transactions. + - You need to enable `send_default_pii` in your client options for this to be enabled, and you can fine-tune the behavior using the new option `max_request_body_size`. +- feat(core): `transaction.set_data` sets data on `TraceContext` (#739) by @lcian + - `transaction.set_data` now sets data on `TraceContext`, as the SDK should not use the `extra` field. +- ref(backtrace): add entries and extra logic for in-app detection (#756) by @lcian +- feat(core): add more frames to be considered not in_app (#760) by @lcian + - The logic used by the SDK to detect `in-app` stack frames has been improved. Now the SDK will mark more frames as not `in-app`. + - A similar improvement has been added to the Sentry [backend](https://github.com/getsentry/sentry/commit/cef4d53e05093d6e9c81c1c49585af86cc135f8b) so that old versions of the SDK can benefit from improved `in-app` reporting. + +### Fixes + +- fix(http): Finish transaction on drop (#727) by @Dav1dde + - Fixed a bug where the current transaction was not finished (hence not sent to Sentry) when its corresponding future was dropped, e.g. due to a panic. +- follow https://github.com/getsentry/sentry-rust/pull/439 for actix-web. fix https://github.com/getsentry/sentry-rust/issues/680 (#737) by @pavel-rosputko + - The HTTP request metadata is now being correctly attached to transactions when using `sentry-actix`. +- fix(tracing): wrap error with synthetic mechanism only if attaching stacktrace (#755) by @lcian + - Fixed a bug that should result in improved grouping and issue titles for events reported by `sentry-tracing` when not capturing stack traces. +- fix(actix): process request in other middleware using correct Hub (#758) by @lcian + - The subsequent middleware in the chain when processing a request now execute within the correct Hub. +- fix(anyhow): attach stacktrace only if error provides backtrace (#759) by @lcian + - Fixed a bug where the SDK was providing incorrect stack traces when capturing an `anyhow` when the `backtrace` feature is enabled but `RUST_BACKTRACE` is not set. + - This should result in correct grouping of the affected issues. + +### Various fixes & improvements + +- Fix CS (#726) by @cleptric +- fix(doctests): update prost (#750) by @lcian +- chore(msrv): bump MSRV to 1.75 (#751) by @lcian +- refactor(actix): simplify body_from_http (#757) by @robjtede +- chore: prepare changelog for release (#761) by @lcian + +### Dependencies + +- build(deps): bump openssl from 0.10.66 to 0.10.70 (#732) by @dependabot +- build(deps): bump ring from 0.17.8 to 0.17.13 (#747) by @dependabot + +## 0.36.0 + +### Various fixes & improvements + +- feat(sentry-tower) Make SentryLayer and SentryService `Sync` if request isn't (#721) by @syphar +- sentry-tower: Update `axum` dependency to v0.8 (#718) by @Turbo87 +- Allow retrieving user of scope (#715) by @thomaseizinger +- Elide lifetimes where possible (#716) by @thomaseizinger +- Replace release bot with GH app (#714) by @Jeffreyhung +- Delay sampling of span to `finish` (#712) by @thomaseizinger + +## 0.35.0 + +**Fixes**: + +- Envelopes will be discarded rather than blocking if the transport channel fills up (previously fixed in async-capable transports, now applied to the curl/ureq transports). ([#701](https://github.com/getsentry/sentry-rust/pull/701)) + +## 0.34.0 + +**Features**: + +- Renamed the `UNSTABLE_metrics` and `UNSTABLE_cadence` feature flags to `metrics` and `metrics-cadence1` respectively. + +## 0.33.0 + +### Various fixes & improvements + +- ref(metrics): Add normalization and update set metrics hashing (#658) by @elramen +- feat: add embedded-svc based http transport (#654) by @madmo + +## 0.32.3 + **Compatiblity**: - Raised the MSRV to **1.73**. @@ -13,6 +391,7 @@ **Updates**: - Updated `reqwest` to version `0.12`. +- Updated `tonic` to version `0.11`. ## 0.32.2 diff --git a/Cargo.lock b/Cargo.lock index aed028cba..14808aa5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,12 +4,12 @@ version = 3 [[package]] name = "actix-codec" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 1.3.2", - "bytes 1.6.0", + "bitflags 2.9.4", + "bytes", "futures-core", "futures-sink", "memchr", @@ -21,25 +21,25 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.4.0" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92ef85799cba03f76e4f7c10f533e66d87c9a7e7055f3391f09000ad8351bc9" +checksum = "0fa882656b67966045e4152c634051e70346939fced7117d5f0b52146a7c74c9" dependencies = [ "actix-codec", "actix-rt", "actix-service", "actix-utils", - "ahash", - "base64 0.21.7", - "bitflags 2.5.0", + "base64", + "bitflags 2.9.4", "brotli", - "bytes 1.6.0", + "bytes", "bytestring", "derive_more", "encoding_rs", "flate2", + "foldhash", "futures-core", - "h2", + "h2 0.3.26", "http 0.2.12", "httparse", "httpdate", @@ -49,8 +49,8 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand 0.8.5", - "sha1 0.10.6", + "rand 0.9.0", + "sha1", "smallvec", "tokio", "tokio-util", @@ -65,27 +65,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.58", + "syn 2.0.100", ] [[package]] name = "actix-router" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" dependencies = [ "bytestring", + "cfg-if", "http 0.2.12", "regex", + "regex-lite", "serde", "tracing", ] [[package]] name = "actix-rt" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" dependencies = [ "futures-core", "tokio", @@ -93,9 +95,9 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.3.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb13e7eef0423ea6eab0e59f6c72e7cb46d33691ad56a726b3cd07ddec2c2d4" +checksum = "6398974fd4284f4768af07965701efbbb5fdc0616bff20cade1bb14b77675e24" dependencies = [ "actix-rt", "actix-service", @@ -103,19 +105,18 @@ dependencies = [ "futures-core", "futures-util", "mio", - "socket2 0.5.6", + "socket2", "tokio", "tracing", ] [[package]] name = "actix-service" -version = "2.0.2" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" dependencies = [ "futures-core", - "paste", "pin-project-lite", ] @@ -131,9 +132,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.4.0" +version = "4.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4a5b5e29603ca8c94a77c65cf874718ceb60292c5a5c3e5f4ace041af462b9" +checksum = "f2e3b15b3dc6c6ed996e4032389e9849d4ab002b1e92fbfe85b5f307d1479b4d" dependencies = [ "actix-codec", "actix-http", @@ -144,15 +145,16 @@ dependencies = [ "actix-service", "actix-utils", "actix-web-codegen", - "ahash", - "bytes 1.6.0", + "bytes", "bytestring", "cfg-if", - "cookie 0.16.2", + "cookie", "derive_more", "encoding_rs", + "foldhash", "futures-core", "futures-util", + "impl-more", "itoa", "language-tags", "log", @@ -160,116 +162,60 @@ dependencies = [ "once_cell", "pin-project-lite", "regex", + "regex-lite", "serde", "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.5.6", - "time 0.3.30", + "socket2", + "time", + "tracing", "url", ] [[package]] name = "actix-web-codegen" -version = "4.2.2" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1f50ebbb30eca122b188319a4398b3f7bb4a8cdf50ecfb73bfc6a3c3ce54f5" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.100", ] [[package]] name = "addr2line" -version = "0.21.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "aead" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" -dependencies = [ - "generic-array", -] - -[[package]] -name = "aes" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" -dependencies = [ - "aes-soft", - "aesni", - "cipher", -] - -[[package]] -name = "aes-gcm" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "aes-soft" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" -dependencies = [ - "cipher", - "opaque-debug", -] - -[[package]] -name = "aesni" -version = "0.10.0" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" -dependencies = [ - "cipher", - "opaque-debug", -] +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] -name = "ahash" -version = "0.8.6" +name = "aho-corasick" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ - "cfg-if", - "getrandom 0.2.12", - "once_cell", - "version_check", - "zerocopy", + "memchr", ] [[package]] -name = "aho-corasick" -version = "1.1.2" +name = "aligned" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "377e4c0ba83e4431b10df45c1d4666f178ea9c552cac93e60c3a88bf32785923" dependencies = [ - "memchr", + "as-slice", ] [[package]] @@ -288,214 +234,52 @@ dependencies = [ ] [[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - -[[package]] -name = "anstyle" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" - -[[package]] -name = "anyhow" -version = "1.0.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" -dependencies = [ - "backtrace", -] - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-channel" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" -dependencies = [ - "concurrent-queue", - "event-listener 4.0.0", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-dup" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7427a12b8dc09291528cfb1da2447059adb4a257388c2acd6497a79d55cf6f7c" -dependencies = [ - "futures-io", - "simple-mutex", -] - -[[package]] -name = "async-executor" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" -dependencies = [ - "async-lock 3.1.2", - "async-task", - "concurrent-queue", - "fastrand 2.0.2", - "futures-lite 2.0.1", - "slab", -] - -[[package]] -name = "async-global-executor" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4353121d5644cdf2beb5726ab752e79a8db1ebb52031770ec47db31d245526" -dependencies = [ - "async-channel 2.1.1", - "async-executor", - "async-io 2.2.1", - "async-lock 3.1.2", - "blocking", - "futures-lite 2.0.1", - "once_cell", -] - -[[package]] -name = "async-h1" -version = "2.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d1d1dae8cb2c4258a79d6ed088b7fb9b4763bf4e9b22d040779761e046a2971" -dependencies = [ - "async-channel 1.9.0", - "async-dup", - "async-global-executor", - "async-io 1.13.0", - "futures-lite 1.13.0", - "http-types", - "httparse", - "log", - "pin-project", -] - -[[package]] -name = "async-io" -version = "1.13.0" +name = "android-tzdata" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.27", - "slab", - "socket2 0.4.10", - "waker-fn", -] +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] -name = "async-io" -version = "2.2.1" +name = "android_system_properties" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6d3b15875ba253d1110c740755e246537483f152fa334f91abd7fe84c88b3ff" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ - "async-lock 3.1.2", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite 2.0.1", - "parking", - "polling 3.3.1", - "rustix 0.38.32", - "slab", - "tracing", - "windows-sys 0.52.0", + "libc", ] [[package]] -name = "async-lock" -version = "2.8.0" +name = "anes" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] -name = "async-lock" -version = "3.1.2" +name = "anstyle" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea8b3453dd7cc96711834b75400d671b73e3656975fa68d9f277163b7f7e316" -dependencies = [ - "event-listener 4.0.0", - "event-listener-strategy", - "pin-project-lite", -] +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] -name = "async-native-tls" -version = "0.3.3" +name = "anyhow" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e9e7a929bd34c68a82d58a4de7f86fffdaf97fb2af850162a7bb19dd7269b33" -dependencies = [ - "async-std", - "native-tls", - "thiserror", - "url", -] +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] -name = "async-std" -version = "1.12.0" +name = "as-slice" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" dependencies = [ - "async-channel 1.9.0", - "async-global-executor", - "async-io 1.13.0", - "async-lock 2.8.0", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite 1.13.0", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", + "stable_deref_trait", ] [[package]] name = "async-stream" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", @@ -504,30 +288,24 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.100", ] -[[package]] -name = "async-task" -version = "4.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" - [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.100", ] [[package]] @@ -538,26 +316,25 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" -version = "0.6.20" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", - "axum-core 0.3.4", - "bitflags 1.3.2", - "bytes 1.6.0", + "axum-core 0.4.5", + "bytes", "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.28", + "http 1.3.1", + "http-body", + "http-body-util", "itoa", - "matchit", + "matchit 0.7.3", "memchr", "mime", "percent-encoding", @@ -565,26 +342,25 @@ dependencies = [ "rustversion", "serde", "sync_wrapper", - "tower", + "tower 0.5.2", "tower-layer", "tower-service", ] [[package]] name = "axum" -version = "0.7.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810a80b128d70e6ed2bdf3fe8ed72c0ae56f5f5948d01c2753282dd92a84fce8" +checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288" dependencies = [ - "async-trait", - "axum-core 0.4.0", - "bytes 1.6.0", + "axum-core 0.5.2", + "bytes", "futures-util", - "http 1.0.0", - "http-body 1.0.0", + "http 1.3.1", + "http-body", "http-body-util", "itoa", - "matchit", + "matchit 0.8.4", "memchr", "mime", "percent-encoding", @@ -592,39 +368,41 @@ dependencies = [ "rustversion", "serde", "sync_wrapper", - "tower", + "tower 0.5.2", "tower-layer", "tower-service", ] [[package]] name = "axum-core" -version = "0.3.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" dependencies = [ "async-trait", - "bytes 1.6.0", + "bytes", "futures-util", - "http 0.2.12", - "http-body 0.4.6", + "http 1.3.1", + "http-body", + "http-body-util", "mime", + "pin-project-lite", "rustversion", + "sync_wrapper", "tower-layer", "tower-service", ] [[package]] name = "axum-core" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0ddc355eab88f4955090a823715df47acf0b7660aab7a69ad5ce6301ee3b73" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ - "async-trait", - "bytes 1.6.0", - "futures-util", - "http 1.0.0", - "http-body 1.0.0", + "bytes", + "futures-core", + "http 1.3.1", + "http-body", "http-body-util", "mime", "pin-project-lite", @@ -636,42 +414,50 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] -[[package]] -name = "base-x" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" - [[package]] name = "base64" -version = "0.13.1" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "base64" -version = "0.21.7" +name = "base64ct" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] -name = "base64" -version = "0.22.0" +name = "bindgen" +version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags 2.9.4", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.100", +] [[package]] name = "bitflags" @@ -681,99 +467,120 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "block-buffer" -version = "0.9.0" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] -name = "block-buffer" -version = "0.10.4" +name = "brotli" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" dependencies = [ - "generic-array", + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", ] [[package]] -name = "blocking" -version = "1.5.1" +name = "brotli-decompressor" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" dependencies = [ - "async-channel 2.1.1", - "async-lock 3.1.2", - "async-task", - "fastrand 2.0.2", - "futures-io", - "futures-lite 2.0.1", - "piper", - "tracing", + "alloc-no-stdlib", + "alloc-stdlib", ] [[package]] -name = "brotli" -version = "3.4.0" +name = "bstr" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", + "memchr", + "serde", ] [[package]] -name = "brotli-decompressor" -version = "2.5.1" +name = "build-time" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +checksum = "f1219c19fc29b7bfd74b7968b420aff5bc951cf517800176e795d6b2300dd382" dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", + "chrono", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] -name = "bytes" -version = "0.5.6" +name = "byteorder" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bytestring" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" +checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" dependencies = [ - "bytes 1.6.0", + "bytes", ] [[package]] -name = "cadence" -version = "0.29.1" +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f39286bc075b023101dccdb79456a1334221c768b8faede0c2aff7ed29a9482d" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" dependencies = [ - "crossbeam-channel", + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 1.0.69", ] [[package]] @@ -784,12 +591,22 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.90" +version = "1.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" dependencies = [ "jobserver", "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", ] [[package]] @@ -799,10 +616,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "ciborium" +name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", @@ -811,43 +646,45 @@ dependencies = [ [[package]] name = "ciborium-io" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", ] [[package]] -name = "cipher" -version = "0.2.5" +name = "clang-sys" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ - "generic-array", + "glob", + "libc", + "libloading", ] [[package]] name = "clap" -version = "4.4.10" +version = "4.5.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fffed7514f420abec6d183b1d3acfd9099c79c3a10a06ade4f8203f1411272" +checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.4.9" +version = "4.5.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63361bae7eef3771745f02d8d892bec2fee5f6e34af316ba556e7f97a7069ff1" +checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5" dependencies = [ "anstyle", "clap_lex", @@ -855,57 +692,37 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.6.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] -name = "concurrent-queue" -version = "2.3.0" +name = "cmake" +version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" dependencies = [ - "crossbeam-utils", + "cc", ] [[package]] -name = "config" -version = "0.10.1" +name = "const_format" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" dependencies = [ - "lazy_static", - "nom", - "serde", + "const_format_proc_macros", ] [[package]] -name = "const_fn" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.14.4" +name = "const_format_proc_macros" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" dependencies = [ - "aes-gcm", - "base64 0.13.1", - "hkdf", - "hmac", - "percent-encoding", - "rand 0.8.5", - "sha2", - "time 0.2.27", - "version_check", + "proc-macro2", + "quote", + "unicode-xid", ] [[package]] @@ -915,7 +732,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ "percent-encoding", - "time 0.3.30", + "time", "version_check", ] @@ -931,30 +748,24 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] -[[package]] -name = "cpuid-bool" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" - [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -996,57 +807,41 @@ dependencies = [ ] [[package]] -name = "crossbeam-channel" -version = "0.5.8" +name = "critical-section" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "crossbeam-deque" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.15" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", - "memoffset", - "scopeguard", ] [[package]] -name = "crossbeam-queue" -version = "0.3.8" +name = "crossbeam-utils" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "crossbeam-utils" -version = "0.8.16" +name = "crunchy" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "crypto-common" @@ -1058,81 +853,77 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6" -dependencies = [ - "generic-array", - "subtle", -] - -[[package]] -name = "ctr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" -dependencies = [ - "cipher", -] - [[package]] name = "curl" -version = "0.4.44" +version = "0.4.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22" +checksum = "d9fb4d13a1be2b58f14d60adba57c9834b78c62fd86c3e76a148f732686e9265" dependencies = [ "curl-sys", "libc", "openssl-probe", "openssl-sys", "schannel", - "socket2 0.4.10", - "winapi", + "socket2", + "windows-sys 0.52.0", ] [[package]] name = "curl-sys" -version = "0.4.68+curl-8.4.0" +version = "0.4.80+curl-8.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a0d18d88360e374b16b2273c832b5e57258ffc1d4aa4f96b108e0738d5752f" +checksum = "55f7df2eac63200c3ab25bde3b2268ef2ee56af3d238e76d61f01c3c49bff734" dependencies = [ "cc", "libc", - "libnghttp2-sys", "libz-sys", "openssl-sys", "pkg-config", "vcpkg", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] -name = "dashmap" -version = "5.5.3" +name = "cvt" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +checksum = "d2ae9bf77fbf2d39ef573205d554d87e86c12f1994e9ea335b0651b9b278bcf1" dependencies = [ "cfg-if", - "hashbrown 0.14.3", - "lock_api", - "once_cell", - "parking_lot_core", ] [[package]] -name = "deadpool" -version = "0.7.0" +name = "darling" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d126179d86aee4556e54f5f3c6bf6d9884e7cc52cef82f77ee6f90a7747616d" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "async-trait", - "config", - "crossbeam-queue", - "num_cpus", - "serde", - "tokio", + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.100", ] [[package]] @@ -1146,321 +937,548 @@ dependencies = [ ] [[package]] -name = "deranged" -version = "0.3.9" +name = "defmt" +version = "0.3.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" dependencies = [ - "powerfmt", + "defmt 1.0.1", ] [[package]] -name = "derive_more" -version = "0.99.17" +name = "defmt" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version 0.4.0", - "syn 1.0.109", + "bitflags 1.3.2", + "defmt-macros", ] [[package]] -name = "digest" -version = "0.9.0" +name = "defmt-macros" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" dependencies = [ - "generic-array", + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] -name = "digest" -version = "0.10.7" +name = "defmt-parser" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "block-buffer 0.10.4", - "crypto-common", + "thiserror 2.0.12", ] [[package]] -name = "discard" -version = "1.0.4" +name = "der" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "pem-rfc7468", + "zeroize", +] [[package]] -name = "either" -version = "1.9.0" +name = "deranged" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] [[package]] -name = "encoding_rs" -version = "0.8.33" +name = "derive_more" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" dependencies = [ - "cfg-if", + "derive_more-impl", ] [[package]] -name = "env_logger" -version = "0.10.1" +name = "derive_more-impl" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", + "proc-macro2", + "quote", + "syn 2.0.100", + "unicode-xid", ] [[package]] -name = "equivalent" -version = "1.0.1" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] [[package]] -name = "erased-serde" -version = "0.3.31" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "serde", + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] -name = "errno" -version = "0.3.8" +name = "either" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] -name = "event-listener" -version = "2.5.3" +name = "embassy-futures" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +checksum = "1f878075b9794c1e4ac788c95b728f26aa6366d32eeb10c7051389f898f7d067" [[package]] -name = "event-listener" -version = "4.0.0" +name = "embassy-sync" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" +checksum = "8d2c8cdff05a7a51ba0087489ea44b0b1d97a296ca6b1d6d1a33ea7423d34049" dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", + "cfg-if", + "critical-section", + "embedded-io-async", + "futures-sink", + "futures-util", + "heapless", ] [[package]] -name = "event-listener-strategy" -version = "0.4.0" +name = "embedded-can" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +checksum = "e9d2e857f87ac832df68fa498d18ddc679175cf3d2e4aa893988e5601baf9438" dependencies = [ - "event-listener 4.0.0", - "pin-project-lite", + "nb 1.1.0", ] [[package]] -name = "fastrand" -version = "1.9.0" +name = "embedded-hal" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" dependencies = [ - "instant", + "nb 0.1.3", + "void", ] [[package]] -name = "fastrand" -version = "2.0.2" +name = "embedded-hal" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" [[package]] -name = "findshlibs" -version = "0.10.2" +name = "embedded-hal-async" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64" +checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" dependencies = [ - "cc", - "lazy_static", - "libc", - "winapi", + "embedded-hal 1.0.0", ] [[package]] -name = "flate2" -version = "1.0.28" +name = "embedded-hal-nb" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "fba4268c14288c828995299e59b12babdbe170f6c6d73731af1b4648142e8605" dependencies = [ - "crc32fast", - "miniz_oxide", + "embedded-hal 1.0.0", + "nb 1.1.0", ] [[package]] -name = "flume" -version = "0.9.2" +name = "embedded-io" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bebadab126f8120d410b677ed95eee4ba6eb7c6dd8e34a5ec88a08050e26132" -dependencies = [ - "futures-core", - "futures-sink", - "spinning_top", -] +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" [[package]] -name = "fnv" -version = "1.0.7" +name = "embedded-io-async" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" +dependencies = [ + "embedded-io", +] [[package]] -name = "foreign-types" -version = "0.3.2" +name = "embedded-svc" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +checksum = "a7770e30ab55cfbf954c00019522490d6ce26a3334bede05a732ba61010e98e0" dependencies = [ - "foreign-types-shared", + "defmt 0.3.100", + "embedded-io", + "embedded-io-async", + "enumset", + "heapless", + "log", + "num_enum", + "serde", + "strum 0.25.0", + "strum_macros 0.25.3", ] [[package]] -name = "foreign-types-shared" -version = "0.1.1" +name = "embuild" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +checksum = "28a8cbd9507fabce8f2741b9ca45da9e898cc2b0f1c2e53d21cb2436aeac811d" +dependencies = [ + "anyhow", + "bindgen", + "bitflags 1.3.2", + "cmake", + "filetime", + "globwalk", + "home", + "log", + "regex", + "remove_dir_all", + "serde", + "serde_json", + "shlex", + "strum 0.24.1", + "tempfile", + "thiserror 1.0.69", + "which", +] [[package]] -name = "form_urlencoded" -version = "1.2.1" +name = "encoding_rs" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "percent-encoding", + "cfg-if", ] [[package]] -name = "futures" -version = "0.3.29" +name = "enumset" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +checksum = "d07a4b049558765cef5f0c1a273c3fc57084d768b44d2f98127aef4cceb17293" dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", + "enumset_derive", + "serde", ] [[package]] -name = "futures-channel" -version = "0.3.30" +name = "enumset_derive" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "59c3b24c345d8c314966bdc1832f6c2635bfcce8e7cf363bd115987bba2ee242" dependencies = [ - "futures-core", - "futures-sink", + "darling", + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] -name = "futures-core" -version = "0.3.30" +name = "env_logger" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" - -[[package]] -name = "futures-executor" -version = "0.3.29" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "envy" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" dependencies = [ - "futures-core", - "futures-task", - "futures-util", + "serde", ] [[package]] -name = "futures-io" -version = "0.3.30" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "futures-lite" -version = "1.13.0" +name = "erased-serde" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" dependencies = [ - "fastrand 1.9.0", - "futures-core", + "serde", +] + +[[package]] +name = "errno" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "esp-idf-hal" +version = "0.45.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775ce25171dc4f615146a4a27ed3a64c6fd99ced77d7112062f2b19bf933f5db" +dependencies = [ + "atomic-waker", + "embassy-sync", + "embedded-can", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-hal-nb", + "embedded-io", + "embedded-io-async", + "embuild", + "enumset", + "esp-idf-sys", + "heapless", + "log", + "nb 1.1.0", + "num_enum", +] + +[[package]] +name = "esp-idf-svc" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc07aaba257d28d54a96af005ca67d0b38876d8837f5d54a3e0547e100b219c" +dependencies = [ + "embassy-futures", + "embedded-hal-async", + "embedded-svc", + "embuild", + "enumset", + "esp-idf-hal", "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", + "heapless", + "log", + "num_enum", + "uncased", ] [[package]] -name = "futures-lite" -version = "2.0.1" +name = "esp-idf-sys" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3831c2651acb5177cbd83943f3d9c8912c5ad03c76afcc0e9511ba568ec5ebb" +checksum = "fb77a3d02b579a60a811ed9be22b78c5e794bc492d833ee7fc44d3a0155885e1" dependencies = [ - "fastrand 2.0.2", + "anyhow", + "build-time", + "cargo_metadata", + "cmake", + "const_format", + "embuild", + "envy", + "libc", + "regex", + "serde", + "strum 0.24.1", + "which", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + +[[package]] +name = "findshlibs" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64" +dependencies = [ + "cc", + "lazy_static", + "libc", + "winapi", +] + +[[package]] +name = "flate2" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_at" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14af6c9694ea25db25baa2a1788703b9e7c6648dcaeeebeb98f7561b5384c036" +dependencies = [ + "aligned", + "cfg-if", + "cvt", + "libc", + "nix", + "windows-sys 0.52.0", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", "futures-core", + "futures-executor", "futures-io", - "memchr", - "parking", - "pin-project-lite", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", ] +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.100", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1486,58 +1504,65 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.1.16" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.9.0+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.2.12" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] -name = "ghash" -version = "0.3.1" +name = "gimli" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375" -dependencies = [ - "opaque-debug", - "polyval", -] +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] -name = "gimli" -version = "0.28.1" +name = "glob" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] -name = "glob" -version = "0.3.1" +name = "globset" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] [[package]] -name = "gloo-timers" -version = "0.2.6" +name = "globwalk" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", + "bitflags 1.3.2", + "ignore", + "walkdir", ] [[package]] @@ -1546,13 +1571,32 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ - "bytes 1.6.0", + "bytes", "fnv", "futures-core", "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.2.6", + "indexmap 2.9.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -1561,9 +1605,22 @@ dependencies = [ [[package]] name = "half" -version = "1.8.2" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hash32" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] [[package]] name = "hashbrown" @@ -1573,15 +1630,32 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "serde", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" [[package]] name = "hex" @@ -1590,34 +1664,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "hkdf" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" -dependencies = [ - "digest 0.9.0", - "hmac", -] - -[[package]] -name = "hmac" -version = "0.10.1" +name = "home" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "crypto-mac", - "digest 0.9.0", + "windows-sys 0.59.0", ] [[package]] name = "hostname" -version = "0.3.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" dependencies = [ + "cfg-if", "libc", - "match_cfg", - "winapi", + "windows-link", ] [[package]] @@ -1626,102 +1689,50 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ - "bytes 1.6.0", + "bytes", "fnv", "itoa", ] [[package]] name = "http" -version = "1.0.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ - "bytes 1.6.0", + "bytes", "fnv", "itoa", ] [[package]] name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes 1.6.0", - "http 0.2.12", - "pin-project-lite", -] - -[[package]] -name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ - "bytes 1.6.0", - "http 1.0.0", + "bytes", + "http 1.3.1", ] [[package]] name = "http-body-util" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" -dependencies = [ - "bytes 1.6.0", - "futures-util", - "http 1.0.0", - "http-body 1.0.0", - "pin-project-lite", -] - -[[package]] -name = "http-client" -version = "6.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1947510dc91e2bf586ea5ffb412caad7673264e14bb39fb9078da114a94ce1a5" -dependencies = [ - "async-h1", - "async-native-tls", - "async-std", - "async-trait", - "cfg-if", - "dashmap", - "deadpool", - "futures", - "http-types", - "isahc", - "log", -] - -[[package]] -name = "http-types" -version = "2.12.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ - "anyhow", - "async-channel 1.9.0", - "async-std", - "base64 0.13.1", - "cookie 0.14.4", - "futures-lite 1.13.0", - "infer", + "bytes", + "futures-core", + "http 1.3.1", + "http-body", "pin-project-lite", - "rand 0.7.3", - "serde", - "serde_json", - "serde_qs", - "serde_urlencoded", - "url", ] [[package]] name = "httparse" -version = "1.8.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -1731,48 +1742,26 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" [[package]] name = "hyper" -version = "0.14.28" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ - "bytes 1.6.0", + "bytes", "futures-channel", - "futures-core", "futures-util", - "h2", - "http 0.2.12", - "http-body 0.4.6", + "h2 0.4.9", + "http 1.3.1", + "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.6", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" -dependencies = [ - "bytes 1.6.0", - "futures-channel", - "futures-util", - "http 1.0.0", - "http-body 1.0.0", - "httparse", - "itoa", - "pin-project-lite", "smallvec", "tokio", "want", @@ -1780,31 +1769,33 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.26.0" +version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", - "http 1.0.0", - "hyper 1.2.0", + "http 1.3.1", + "hyper", "hyper-util", - "rustls 0.22.3", + "rustls", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] name = "hyper-timeout" -version = "0.4.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 0.14.28", + "hyper", + "hyper-util", "pin-project-lite", "tokio", - "tokio-io-timeout", + "tower-service", ] [[package]] @@ -1813,9 +1804,9 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ - "bytes 1.6.0", + "bytes", "http-body-util", - "hyper 1.2.0", + "hyper", "hyper-util", "native-tls", "tokio", @@ -1825,173 +1816,305 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ - "bytes 1.6.0", + "bytes", "futures-channel", "futures-util", - "http 1.0.0", - "http-body 1.0.0", - "hyper 1.2.0", + "http 1.3.1", + "http-body", + "hyper", + "libc", "pin-project-lite", - "socket2 0.5.6", + "socket2", "tokio", - "tower", "tower-service", "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", ] [[package]] -name = "indexmap" -version = "1.9.3" +name = "idna_adapter" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ - "autocfg", - "hashbrown 0.12.3", + "icu_normalizer", + "icu_properties", ] [[package]] -name = "indexmap" -version = "2.2.6" +name = "ignore" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" dependencies = [ - "equivalent", - "hashbrown 0.14.3", + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", ] [[package]] -name = "infer" -version = "0.2.3" +name = "impl-more" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] -name = "instant" -version = "0.1.12" +name = "indexmap" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ - "cfg-if", + "autocfg", + "hashbrown 0.12.3", ] [[package]] -name = "io-lifetimes" -version = "1.0.11" +name = "indexmap" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", + "equivalent", + "hashbrown 0.15.2", ] [[package]] name = "ipnet" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is-terminal" -version = "0.4.9" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", - "rustix 0.38.32", - "windows-sys 0.48.0", + "libc", + "windows-sys 0.59.0", ] [[package]] -name = "isahc" -version = "0.9.14" +name = "itertools" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2948a0ce43e2c2ef11d7edf6816508998d99e13badd1150be0914205df9388a" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ - "bytes 0.5.6", - "crossbeam-utils", - "curl", - "curl-sys", - "encoding_rs", - "flume", - "futures-lite 1.13.0", - "http 0.2.12", - "log", - "mime", - "once_cell", - "slab", - "sluice", - "tracing", - "tracing-futures", - "url", - "waker-fn", + "either", ] [[package]] name = "itertools" -version = "0.10.5" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itertools" -version = "0.11.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.28" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ + "getrandom 0.3.2", "libc", ] [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - [[package]] name = "language-tags" version = "0.3.2" @@ -2000,44 +2123,42 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "lexical-core" -version = "0.7.6" +name = "libc" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" -dependencies = [ - "arrayvec", - "bitflags 1.3.2", - "cfg-if", - "ryu", - "static_assertions", -] +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] -name = "libc" -version = "0.2.153" +name = "libloading" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] [[package]] -name = "libnghttp2-sys" -version = "0.1.8+1.55.1" +name = "libredox" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fae956c192dadcdb5dace96db71fa0b827333cce7c7b38dc71446f024d8a340" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "cc", + "bitflags 2.9.4", "libc", + "redox_syscall", ] [[package]] name = "libz-sys" -version = "1.1.12" +version = "1.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" dependencies = [ "cc", "libc", @@ -2047,15 +2168,21 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "local-channel" @@ -2076,9 +2203,9 @@ checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -2086,18 +2213,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" -dependencies = [ - "value-bag", -] - -[[package]] -name = "match_cfg" -version = "0.1.0" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "matchit" @@ -2106,19 +2224,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] -name = "memchr" -version = "2.7.2" +name = "matchit" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] -name = "memoffset" -version = "0.9.0" +name = "memchr" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" @@ -2127,43 +2242,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] -name = "mime_guess" -version = "2.0.4" +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" -dependencies = [ - "mime", - "unicase", -] +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ - "adler", + "adler2", ] [[package]] name = "mio" -version = "0.8.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -2175,80 +2285,125 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" -version = "5.1.3" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ - "lexical-core", "memchr", - "version_check", + "minimal-lexical", +] + +[[package]] +name = "normpath" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed" +dependencies = [ + "windows-sys 0.59.0", ] [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] -name = "num_cpus" -version = "1.16.0" +name = "num_enum" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" dependencies = [ - "hermit-abi", - "libc", + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] name = "object" -version = "0.32.2" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oorandom" -version = "11.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" - -[[package]] -name = "opaque-debug" -version = "0.3.0" +version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.9.4", "cfg-if", "foreign-types", "libc", @@ -2265,20 +2420,20 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.100", ] [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", @@ -2287,33 +2442,54 @@ dependencies = [ ] [[package]] -name = "os_info" -version = "3.7.0" +name = "opentelemetry" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" +checksum = "9e87237e2775f74896f9ad219d26a2081751187eb7c9f5c58dde20a23b95d16c" dependencies = [ - "log", - "serde", - "winapi", + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.12", + "tracing", ] [[package]] -name = "overload" -version = "0.1.1" +name = "opentelemetry_sdk" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +checksum = "afdefb21d1d47394abc1ba6c57363ab141be19e27cc70d0e422b7f303e4d290b" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "opentelemetry", + "percent-encoding", + "rand 0.9.0", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tokio-stream", +] [[package]] -name = "parking" -version = "2.2.0" +name = "os_info" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "2a604e53c24761286860eba4e2c8b23a0161526476b1de520139d69cdb85a6b5" +dependencies = [ + "log", + "serde", + "windows-sys 0.52.0", +] [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -2321,22 +2497,25 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] -name = "paste" -version = "1.0.14" +name = "pem-rfc7468" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] [[package]] name = "percent-encoding" @@ -2346,29 +2525,29 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.100", ] [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -2376,28 +2555,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "piper" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" -dependencies = [ - "atomic-waker", - "fastrand 2.0.2", - "futures-io", -] - [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plotters" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", @@ -2408,142 +2576,186 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] [[package]] -name = "polling" -version = "2.8.0" +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] -name = "polling" -version = "3.3.1" +name = "ppv-lite86" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf63fa624ab313c11656b4cda960bfc46c410187ad493c41f6ba2d8c1e991c9e" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "cfg-if", - "concurrent-queue", - "pin-project-lite", - "rustix 0.38.32", - "tracing", - "windows-sys 0.52.0", + "zerocopy", ] [[package]] -name = "polyval" -version = "0.4.5" +name = "pretty_env_logger" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" dependencies = [ - "cpuid-bool", - "opaque-debug", - "universal-hash", + "env_logger", + "log", ] [[package]] -name = "powerfmt" -version = "0.2.0" +name = "prettyplease" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +dependencies = [ + "proc-macro2", + "syn 2.0.100", +] [[package]] -name = "ppv-lite86" -version = "0.2.17" +name = "proc-macro-crate" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] [[package]] -name = "pretty_env_logger" -version = "0.5.0" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" dependencies = [ - "env_logger", - "log", + "proc-macro2", + "quote", ] [[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" +name = "proc-macro-error2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.100", +] [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.12.3" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ - "bytes 1.6.0", + "bytes", "prost-derive", ] [[package]] name = "prost-derive" -version = "0.12.3" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.100", ] [[package]] -name = "quote" -version = "1.0.35" +name = "quinn" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" dependencies = [ - "proc-macro2", + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", ] [[package]] -name = "rand" -version = "0.7.3" +name = "quinn-proto" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" +dependencies = [ + "bytes", + "getrandom 0.3.2", + "rand 0.9.0", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" dependencies = [ - "getrandom 0.1.16", + "cfg_aliases", "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", ] +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rand" version = "0.8.5" @@ -2556,13 +2768,14 @@ dependencies = [ ] [[package]] -name = "rand_chacha" -version = "0.2.2" +name = "rand" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", + "rand_chacha 0.9.0", + "rand_core 0.9.3", + "zerocopy", ] [[package]] @@ -2576,12 +2789,13 @@ dependencies = [ ] [[package]] -name = "rand_core" -version = "0.5.1" +name = "rand_chacha" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ - "getrandom 0.1.16", + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -2590,23 +2804,23 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.12", + "getrandom 0.2.15", ] [[package]] -name = "rand_hc" -version = "0.2.0" +name = "rand_core" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "rand_core 0.5.1", + "getrandom 0.3.2", ] [[package]] name = "rayon" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -2614,9 +2828,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -2624,18 +2838,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.4", ] [[package]] name = "regex" -version = "1.10.2" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -2645,42 +2859,62 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "relative-path" -version = "1.9.0" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "remove_dir_all" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca" +checksum = "a694f9e0eb3104451127f6cc1e5de55f59d3b1fc8c5ddfaeb6f1e716479ceb4a" +dependencies = [ + "cfg-if", + "cvt", + "fs_at", + "libc", + "normpath", + "windows-sys 0.59.0", +] [[package]] name = "reqwest" -version = "0.12.3" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e6cc1e89e689536eb5aeede61520e874df5a4707df811cd5da4aa5fbb2aae19" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" dependencies = [ - "base64 0.22.0", - "bytes 1.6.0", + "base64", + "bytes", "futures-channel", "futures-core", "futures-util", - "http 1.0.0", - "http-body 1.0.0", + "http 1.3.1", + "http-body", "http-body-util", - "hyper 1.2.0", + "hyper", "hyper-rustls", "hyper-tls", "hyper-util", @@ -2692,7 +2926,8 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.22.3", + "quinn", + "rustls", "rustls-pemfile", "rustls-pki-types", "serde", @@ -2702,167 +2937,145 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls", + "tower 0.5.2", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.26.1", - "winreg", + "webpki-roots", + "windows-registry", ] [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.12", + "getrandom 0.2.15", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rstest" -version = "0.18.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +checksum = "6fc39292f8613e913f7df8fa892b8944ceb47c247b78e1b1ae2f09e019be789d" dependencies = [ - "futures", "futures-timer", + "futures-util", "rstest_macros", - "rustc_version 0.4.0", + "rustc_version", ] [[package]] name = "rstest_macros" -version = "0.18.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" dependencies = [ "cfg-if", "glob", + "proc-macro-crate", "proc-macro2", "quote", "regex", "relative-path", - "rustc_version 0.4.0", - "syn 2.0.58", + "rustc_version", + "syn 2.0.100", "unicode-ident", ] [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] -name = "rustc_version" -version = "0.2.3" +name = "rustc-hash" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver 0.9.0", -] +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.20", + "semver", ] [[package]] name = "rustix" -version = "0.37.27" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.4", "errno", - "io-lifetimes", "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", ] [[package]] name = "rustix" -version = "0.38.32" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys 0.4.13", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustls" -version = "0.21.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.22.3" +version = "0.23.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99008d7ad0bbbea527ec27bddbc0e432c5b87d8175178cee68d2eec9c4a1813c" +checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" dependencies = [ "log", + "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.102.2", + "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-pemfile" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.21.7", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" - -[[package]] -name = "rustls-webpki" -version = "0.101.7" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" dependencies = [ - "ring", - "untrusted", + "web-time", ] [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.103.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" dependencies = [ "ring", "rustls-pki-types", @@ -2871,15 +3084,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -2892,11 +3105,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2905,23 +3118,13 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "security-framework" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.4", "core-foundation", "core-foundation-sys", "libc", @@ -2930,9 +3133,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.10.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -2940,66 +3143,56 @@ dependencies = [ [[package]] name = "semver" -version = "0.9.0" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" dependencies = [ - "semver-parser", + "serde", ] -[[package]] -name = "semver" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" - -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - [[package]] name = "sentry" -version = "0.32.2" +version = "0.45.0" dependencies = [ "actix-web", "anyhow", "curl", - "http-client", + "embedded-svc", + "esp-idf-svc", "httpdate", - "isahc", "log", "native-tls", "pretty_env_logger", "reqwest", - "rustls 0.21.10", + "rustls", + "sentry-actix", "sentry-anyhow", "sentry-backtrace", "sentry-contexts", "sentry-core", "sentry-debug-images", "sentry-log", + "sentry-opentelemetry", "sentry-panic", "sentry-slog", "sentry-tower", "sentry-tracing", "serde_json", "slog", - "surf", "tokio", - "tower", + "tower 0.5.2", "tracing", "tracing-subscriber", "ureq", - "webpki-roots 0.25.4", ] [[package]] name = "sentry-actix" -version = "0.32.2" +version = "0.45.0" dependencies = [ + "actix-http", "actix-web", + "bytes", "futures", "futures-util", "sentry", @@ -3009,7 +3202,7 @@ dependencies = [ [[package]] name = "sentry-anyhow" -version = "0.32.2" +version = "0.45.0" dependencies = [ "anyhow", "sentry", @@ -3019,22 +3212,21 @@ dependencies = [ [[package]] name = "sentry-backtrace" -version = "0.32.2" +version = "0.45.0" dependencies = [ "backtrace", - "once_cell", "regex", "sentry-core", ] [[package]] name = "sentry-contexts" -version = "0.32.2" +version = "0.45.0" dependencies = [ "hostname", "libc", "os_info", - "rustc_version 0.4.0", + "rustc_version", "sentry", "sentry-core", "uname", @@ -3042,47 +3234,56 @@ dependencies = [ [[package]] name = "sentry-core" -version = "0.32.2" +version = "0.45.0" dependencies = [ "anyhow", - "cadence", "criterion", "futures", "log", - "once_cell", - "rand 0.8.5", + "rand 0.9.0", "rayon", "sentry", "sentry-types", "serde", "serde_json", - "thiserror", + "thiserror 2.0.12", "tokio", + "url", "uuid", ] [[package]] name = "sentry-debug-images" -version = "0.32.2" +version = "0.45.0" dependencies = [ "findshlibs", - "once_cell", "sentry-core", ] [[package]] name = "sentry-log" -version = "0.32.2" +version = "0.45.0" dependencies = [ + "bitflags 2.9.4", "log", "pretty_env_logger", "sentry", "sentry-core", ] +[[package]] +name = "sentry-opentelemetry" +version = "0.45.0" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "sentry", + "sentry-core", +] + [[package]] name = "sentry-panic" -version = "0.32.2" +version = "0.45.0" dependencies = [ "sentry", "sentry-backtrace", @@ -3091,7 +3292,7 @@ dependencies = [ [[package]] name = "sentry-slog" -version = "0.32.2" +version = "0.45.0" dependencies = [ "erased-serde", "sentry", @@ -3103,11 +3304,11 @@ dependencies = [ [[package]] name = "sentry-tower" -version = "0.32.2" +version = "0.45.0" dependencies = [ "anyhow", - "axum 0.7.1", - "http 1.0.0", + "axum 0.8.3", + "http 1.3.1", "pin-project", "prost", "sentry", @@ -3115,7 +3316,7 @@ dependencies = [ "sentry-core", "tokio", "tonic", - "tower", + "tower 0.5.2", "tower-layer", "tower-service", "url", @@ -3123,12 +3324,14 @@ dependencies = [ [[package]] name = "sentry-tracing" -version = "0.32.2" +version = "0.45.0" dependencies = [ + "bitflags 2.9.4", "log", "sentry", "sentry-backtrace", "sentry-core", + "serde_json", "tokio", "tracing", "tracing-core", @@ -3137,62 +3340,52 @@ dependencies = [ [[package]] name = "sentry-types" -version = "0.32.2" +version = "0.45.0" dependencies = [ "debugid", "hex", - "rand 0.8.5", + "rand 0.9.0", "rstest", "serde", "serde_json", - "thiserror", - "time 0.3.30", + "thiserror 2.0.12", + "time", "url", "uuid", ] [[package]] name = "serde" -version = "1.0.197" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.100", ] [[package]] name = "serde_json" -version = "1.0.115" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] -[[package]] -name = "serde_qs" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" -dependencies = [ - "percent-encoding", - "serde", - "thiserror", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3205,15 +3398,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha1" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" -dependencies = [ - "sha1_smol", -] - [[package]] name = "sha1" version = "0.10.6" @@ -3222,26 +3406,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "sha1_smol" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", + "digest", ] [[package]] @@ -3254,21 +3419,18 @@ dependencies = [ ] [[package]] -name = "signal-hook-registry" -version = "1.4.1" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "simple-mutex" -version = "1.1.5" +name = "signal-hook-registry" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38aabbeafa6f6dead8cebf246fe9fae1f9215c8d29b3a69f93bd62a9e4a3dcd6" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ - "event-listener 2.5.3", + "libc", ] [[package]] @@ -3289,149 +3451,77 @@ dependencies = [ "erased-serde", ] -[[package]] -name = "sluice" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" -dependencies = [ - "async-channel 1.9.0", - "futures-core", - "futures-io", -] - [[package]] name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "socket2" -version = "0.4.10" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "spinning_top" -version = "0.2.5" +name = "stable_deref_trait" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9eb1a2f4c41445a3a0ff9abc5221c5fcd28e1f13cd7c0397706f9ac938ddb0" -dependencies = [ - "lock_api", -] +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] -name = "standback" -version = "0.2.17" +name = "strum" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" dependencies = [ - "version_check", + "strum_macros 0.24.3", ] [[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "stdweb" -version = "0.4.20" +name = "strum" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" dependencies = [ - "discard", - "rustc_version 0.2.3", - "stdweb-derive", - "stdweb-internal-macros", - "stdweb-internal-runtime", - "wasm-bindgen", + "strum_macros 0.25.3", ] [[package]] -name = "stdweb-derive" -version = "0.5.3" +name = "strum_macros" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ + "heck", "proc-macro2", "quote", - "serde", - "serde_derive", + "rustversion", "syn 1.0.109", ] [[package]] -name = "stdweb-internal-macros" -version = "0.2.9" +name = "strum_macros" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ - "base-x", + "heck", "proc-macro2", "quote", - "serde", - "serde_derive", - "serde_json", - "sha1 0.6.1", - "syn 1.0.109", + "rustversion", + "syn 2.0.100", ] -[[package]] -name = "stdweb-internal-runtime" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" - [[package]] name = "subtle" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" - -[[package]] -name = "surf" -version = "2.3.2" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718b1ae6b50351982dedff021db0def601677f2120938b070eadb10ba4038dd7" -dependencies = [ - "async-native-tls", - "async-std", - "async-trait", - "cfg-if", - "futures-util", - "getrandom 0.2.12", - "http-client", - "http-types", - "log", - "mime_guess", - "once_cell", - "pin-project-lite", - "serde", - "serde_json", -] +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -3446,9 +3536,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.58" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -3457,126 +3547,135 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "0.1.2" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] [[package]] name = "tempfile" -version = "3.10.1" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ - "cfg-if", - "fastrand 2.0.2", - "rustix 0.38.32", - "windows-sys 0.52.0", + "fastrand", + "getrandom 0.3.2", + "once_cell", + "rustix 1.0.5", + "windows-sys 0.59.0", ] [[package]] name = "termcolor" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.12", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.100", ] [[package]] -name = "thread_local" -version = "1.1.7" +name = "thiserror-impl" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ - "cfg-if", - "once_cell", + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] -name = "time" -version = "0.2.27" +name = "thread_local" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ - "const_fn", - "libc", - "standback", - "stdweb", - "time-macros 0.1.1", - "version_check", - "winapi", + "cfg-if", + "once_cell", ] [[package]] name = "time" -version = "0.3.30" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", - "time-macros 0.2.15", + "time-macros", ] [[package]] name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.1.1" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" -dependencies = [ - "proc-macro-hack", - "time-macros-impl", -] +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ + "num-conv", "time-core", ] [[package]] -name = "time-macros-impl" -version = "0.1.2" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ - "proc-macro-hack", - "proc-macro2", - "quote", - "standback", - "syn 1.0.109", + "displaydoc", + "zerovec", ] [[package]] @@ -3591,9 +3690,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] @@ -3606,42 +3705,31 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", - "bytes 1.6.0", + "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", - "signal-hook-registry", - "socket2 0.5.6", - "tokio-macros", - "windows-sys 0.48.0", -] - -[[package]] -name = "tokio-io-timeout" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" -dependencies = [ - "pin-project-lite", - "tokio", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.100", ] [[package]] @@ -3656,20 +3744,19 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.25.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.22.3", - "rustls-pki-types", + "rustls", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -3678,40 +3765,59 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" dependencies = [ - "bytes 1.6.0", + "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap 2.9.0", + "toml_datetime", + "winnow", ] [[package]] name = "tonic" -version = "0.10.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ "async-stream", "async-trait", - "axum 0.6.20", - "base64 0.21.7", - "bytes 1.6.0", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.28", + "axum 0.7.9", + "base64", + "bytes", + "h2 0.4.9", + "http 1.3.1", + "http-body", + "http-body-util", + "hyper", "hyper-timeout", + "hyper-util", "percent-encoding", "pin-project", "prost", + "socket2", "tokio", "tokio-stream", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -3737,23 +3843,38 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -3763,35 +3884,25 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.100", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", ] -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - [[package]] name = "tracing-log" version = "0.2.0" @@ -3805,9 +3916,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "nu-ansi-term", "sharded-slab", @@ -3825,9 +3936,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "uname" @@ -3839,44 +3950,25 @@ dependencies = [ ] [[package]] -name = "unicase" -version = "2.7.0" +name = "uncased" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" dependencies = [ "version_check", ] -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - [[package]] name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-normalization" -version = "0.1.23" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] -name = "universal-hash" -version = "0.4.0" +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" -dependencies = [ - "generic-array", - "subtle", -] +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "untrusted" @@ -3886,25 +3978,41 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.9.1" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" +checksum = "b7a3e9af6113ecd57b8c63d3cd76a385b2e3881365f1f489e54f49801d0c83ea" dependencies = [ - "base64 0.21.7", + "base64", + "der", "log", "native-tls", - "once_cell", - "rustls 0.21.10", - "rustls-webpki 0.101.7", - "url", - "webpki-roots 0.25.4", + "percent-encoding", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "ureq-proto", + "utf-8", + "webpki-root-certs 0.26.11", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadf18427d33828c311234884b7ba2afb57143e6e7e69fda7ee883b624661e36" +dependencies = [ + "base64", + "http 1.3.1", + "httparse", + "log", ] [[package]] name = "url" -version = "2.5.0" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -3912,27 +4020,39 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "uuid" -version = "1.6.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ - "getrandom 0.2.12", + "getrandom 0.3.2", "serde", ] [[package]] name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - -[[package]] -name = "value-bag" -version = "1.8.1" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74797339c3b98616c009c7c3eb53a0ce41e85c8ec66bd3db96ed132d20cfdee8" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcpkg" @@ -3942,21 +4062,21 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "waker-fn" -version = "1.1.1" +name = "void" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -3973,58 +4093,63 @@ dependencies = [ [[package]] name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.100", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4032,48 +4157,85 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.100", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "webpki-roots" -version = "0.25.4" +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.0", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "01a83f7e1a9f8712695c03eabe9ed3fbca0feff0152f33f12593e5a6303cb1a4" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "webpki-roots" -version = "0.26.1" +version = "0.26.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "winapi" version = "0.3.9" @@ -4092,11 +4254,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -4106,12 +4268,82 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings 0.4.0", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings 0.3.1", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ - "windows-targets 0.48.5", + "windows-link", ] [[package]] @@ -4120,183 +4352,292 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] name = "windows-targets" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] -name = "winreg" -version = "0.52.0" +name = "winnow" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" dependencies = [ - "cfg-if", - "windows-sys 0.48.0", + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.4", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", ] [[package]] name = "zerocopy" -version = "0.7.31" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.31" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.100", + "synstructure", ] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] [[package]] name = "zstd" -version = "0.12.4" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "6.0.6" +version = "7.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" dependencies = [ - "libc", "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" +version = "2.0.15+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 48263534c..1b39197e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "sentry-core", "sentry-debug-images", "sentry-log", + "sentry-opentelemetry", "sentry-panic", "sentry-slog", "sentry-tower", diff --git a/LICENSE b/LICENSE index d97d9399c..3e4988928 100644 --- a/LICENSE +++ b/LICENSE @@ -1,203 +1,21 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 Functional Software, Inc. dba Sentry (https://sentry.io) - and individual contributors. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2021 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index daf0d92b4..685a92bca 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,11 @@ lint: cargo +stable clippy --all-features --tests --examples -- -D clippy::all .PHONY: lint +fix: + @rustup component add clippy --toolchain stable 2> /dev/null + cargo +stable clippy --all-features --workspace --tests --examples --fix -- -D clippy::all +.PHONY: fix + # Tests test: checkall testall diff --git a/README.md b/README.md index 1c1014207..6370374e3 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,12 @@ This workspace contains various crates that provide support for logging events a An integration for the `log` and `env_logger` crate. +- [sentry-opentelemetry](./sentry-opentelemetry) + [![crates.io](https://img.shields.io/crates/v/sentry-opentelemetry.svg)](https://crates.io/crates/sentry-opentelemetry) + [![docs.rs](https://docs.rs/sentry-opentelemetry/badge.svg)](https://docs.rs/sentry-opentelemetry) + + An integration for the `opentelemetry` crate. + - [sentry-panic](./sentry-panic) [![crates.io](https://img.shields.io/crates/v/sentry-panic.svg)](https://crates.io/crates/sentry-panic) [![docs.rs](https://docs.rs/sentry-panic/badge.svg)](https://docs.rs/sentry-panic) @@ -93,7 +99,7 @@ best API and adding new features. We currently only verify this crate against a recent version of Sentry hosted on [sentry.io](https://sentry.io/) but it should work with on-prem Sentry versions 20.6 and later. -The **Minimum Supported Rust Version** is currently at _1.73.0_. +The **Minimum Supported Rust Version** is currently at _1.81.0_. The Sentry crates tries to support a _6 months_ old Rust version at time of release, and the MSRV will be increased in accordance with its dependencies. diff --git a/clippy.toml b/clippy.toml index f09ac0fb5..5e90250c4 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.73.0" +msrv = "1.81.0" diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index 490ab6b6d..131a80fbd 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -14,3 +14,4 @@ perl -pi -e "s/^version = \".*?\"/version = \"$NEW_VERSION\"/" sentry*/Cargo.tom perl -pi -e "s/^(sentry.*)?version = \".*?\"/\$1version = \"$NEW_VERSION\"/" sentry*/Cargo.toml "$SCRIPT_DIR"/update-readme.sh "$NEW_VERSION" +cargo metadata --format-version 1 > /dev/null # update `Cargo.lock` diff --git a/sentry-actix/Cargo.toml b/sentry-actix/Cargo.toml index ba337e25d..06f88f443 100644 --- a/sentry-actix/Cargo.toml +++ b/sentry-actix/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "sentry-actix" -version = "0.32.2" +version = "0.45.0" authors = ["Sentry "] -license = "Apache-2.0" +license = "MIT" readme = "README.md" repository = "https://github.com/getsentry/sentry-rust" homepage = "https://sentry.io/welcome/" @@ -10,17 +10,23 @@ description = """ Sentry client extension for actix-web 3. """ edition = "2021" -rust-version = "1.73" +rust-version = "1.81" + +[features] +default = ["release-health"] +release-health = ["sentry-core/release-health"] [dependencies] actix-web = { version = "4", default-features = false } +bytes = "1.2" futures-util = { version = "0.3.5", default-features = false } -sentry-core = { version = "0.32.2", path = "../sentry-core", default-features = false, features = [ +sentry-core = { version = "0.45.0", path = "../sentry-core", default-features = false, features = [ "client", ] } +actix-http = "3.10" [dev-dependencies] actix-web = { version = "4" } futures = "0.3" sentry = { path = "../sentry", features = ["test"] } -tokio = { version = "1.0", features = ["full"] } +tokio = { version = "1.44", features = ["full"] } diff --git a/sentry-actix/LICENSE b/sentry-actix/LICENSE index d97d9399c..3e4988928 100644 --- a/sentry-actix/LICENSE +++ b/sentry-actix/LICENSE @@ -1,203 +1,21 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 Functional Software, Inc. dba Sentry (https://sentry.io) - and individual contributors. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2021 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sentry-actix/README.md b/sentry-actix/README.md index 6255da8a3..04263046e 100644 --- a/sentry-actix/README.md +++ b/sentry-actix/README.md @@ -11,8 +11,7 @@ report them to `Sentry`. To use this middleware just configure Sentry and then add it to your actix web app as a middleware. Because actix is generally working with non sendable objects and highly concurrent -this middleware creates a new hub per request. As a result many of the sentry integrations -such as breadcrumbs do not work unless you bind the actix hub. +this middleware creates a new Hub per request. ## Example @@ -67,15 +66,19 @@ let _sentry = sentry::init(sentry::ClientOptions { ## Reusing the Hub This integration will automatically create a new per-request Hub from the main Hub, and update the -current Hub instance. For example, the following will capture a message in the current request's Hub: +current Hub instance. For example, the following in the handler or in any of the subsequent +middleware will capture a message in the current request's Hub: ```rust sentry::capture_message("Something is not well", sentry::Level::Warning); ``` +It is recommended to register the Sentry middleware as the last, i.e. the first to be executed +when processing a request, so that the rest of the processing will run with the correct Hub. + ## Resources -License: Apache-2.0 +License: MIT - [Discord](https://discord.gg/ez5KZN7) server for project discussions. - Follow [@getsentry](https://twitter.com/getsentry) on Twitter for updates diff --git a/sentry-actix/examples/basic.rs b/sentry-actix/examples/basic.rs index 6e4544fbc..acde8561a 100644 --- a/sentry-actix/examples/basic.rs +++ b/sentry-actix/examples/basic.rs @@ -11,7 +11,7 @@ async fn healthy(_req: HttpRequest) -> Result { #[get("/err")] async fn errors(_req: HttpRequest) -> Result { - Err(io::Error::new(io::ErrorKind::Other, "An error happens here").into()) + Err(io::Error::other("An error happens here").into()) } #[get("/msg")] diff --git a/sentry-actix/src/lib.rs b/sentry-actix/src/lib.rs index 376854d4a..2a69a41c7 100644 --- a/sentry-actix/src/lib.rs +++ b/sentry-actix/src/lib.rs @@ -3,8 +3,7 @@ //! //! To use this middleware just configure Sentry and then add it to your actix web app as a //! middleware. Because actix is generally working with non sendable objects and highly concurrent -//! this middleware creates a new hub per request. As a result many of the sentry integrations -//! such as breadcrumbs do not work unless you bind the actix hub. +//! this middleware creates a new Hub per request. //! //! # Example //! @@ -59,11 +58,15 @@ //! # Reusing the Hub //! //! This integration will automatically create a new per-request Hub from the main Hub, and update the -//! current Hub instance. For example, the following will capture a message in the current request's Hub: +//! current Hub instance. For example, the following in the handler or in any of the subsequent +//! middleware will capture a message in the current request's Hub: //! //! ``` //! sentry::capture_message("Something is not well", sentry::Level::Warning); //! ``` +//! +//! It is recommended to register the Sentry middleware as the last, i.e. the first to be executed +//! when processing a request, so that the rest of the processing will run with the correct Hub. #![doc(html_favicon_url = "https://sentry-brand.storage.googleapis.com/favicon.ico")] #![doc(html_logo_url = "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png")] @@ -73,15 +76,20 @@ use std::borrow::Cow; use std::pin::Pin; +use std::rc::Rc; use std::sync::Arc; +use actix_http::header::{self, HeaderMap}; use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; use actix_web::http::StatusCode; use actix_web::Error; +use bytes::{Bytes, BytesMut}; use futures_util::future::{ok, Future, Ready}; -use futures_util::FutureExt; +use futures_util::{FutureExt as _, TryStreamExt as _}; use sentry_core::protocol::{self, ClientSdkPackage, Event, Request}; +use sentry_core::utils::{is_sensitive_header, scrub_pii_from_url}; +use sentry_core::MaxRequestBodySize; use sentry_core::{Hub, SentryFutureExt}; /// A helper construct that can be used to reconfigure and build the middleware. @@ -180,7 +188,7 @@ impl Default for Sentry { impl Transform for Sentry where - S: Service, Error = Error>, + S: Service, Error = Error> + 'static, S::Future: 'static, { type Response = ServiceResponse; @@ -191,7 +199,7 @@ where fn new_transform(&self, service: S) -> Self::Future { ok(SentryMiddleware { - service, + service: Rc::new(service), inner: self.clone(), }) } @@ -199,13 +207,63 @@ where /// The middleware for individual services. pub struct SentryMiddleware { - service: S, + service: Rc, inner: Sentry, } +fn should_capture_request_body( + headers: &HeaderMap, + with_pii: bool, + max_request_body_size: MaxRequestBodySize, +) -> bool { + let is_chunked = headers + .get(header::TRANSFER_ENCODING) + .and_then(|h| h.to_str().ok()) + .map(|transfer_encoding| transfer_encoding.contains("chunked")) + .unwrap_or(false); + + let is_valid_content_type = with_pii + || headers + .get(header::CONTENT_TYPE) + .and_then(|h| h.to_str().ok()) + .is_some_and(|content_type| { + matches!( + content_type, + "application/json" | "application/x-www-form-urlencoded" + ) + }); + + let is_within_size_limit = headers + .get(header::CONTENT_LENGTH) + .and_then(|h| h.to_str().ok()) + .and_then(|content_length| content_length.parse::().ok()) + .map(|content_length| max_request_body_size.is_within_size_limit(content_length)) + .unwrap_or(false); + + !is_chunked && is_valid_content_type && is_within_size_limit +} + +/// Extract a body from the HTTP request +async fn body_from_http(req: &mut ServiceRequest) -> actix_web::Result { + let stream = req.extract::().await?; + let body = stream.try_collect::().await?.freeze(); + + // put copy of payload back into request for downstream to read + req.set_payload(actix_web::dev::Payload::from(body.clone())); + + Ok(body) +} + +async fn capture_request_body(req: &mut ServiceRequest) -> String { + match body_from_http(req).await { + Ok(request_body) => String::from_utf8_lossy(&request_body).into_owned(), + Err(_) => String::new(), + } +} + impl Service for SentryMiddleware where - S: Service, Error = Error>, + S: Service, Error = Error> + 'static, S::Future: 'static, { type Response = ServiceResponse; @@ -224,20 +282,31 @@ where let hub = Arc::new(Hub::new_from_top( inner.hub.clone().unwrap_or_else(Hub::main), )); + let client = hub.client(); - let track_sessions = client.as_ref().map_or(false, |client| { - let options = client.options(); - options.auto_session_tracking - && options.session_mode == sentry_core::SessionMode::Request - }); - if track_sessions { - hub.start_session(); + + let max_request_body_size = client + .as_ref() + .map(|client| client.options().max_request_body_size) + .unwrap_or(MaxRequestBodySize::None); + + #[cfg(feature = "release-health")] + { + let track_sessions = client.as_ref().is_some_and(|client| { + let options = client.options(); + options.auto_session_tracking + && options.session_mode == sentry_core::SessionMode::Request + }); + if track_sessions { + hub.start_session(); + } } + let with_pii = client .as_ref() - .map_or(false, |client| client.options().send_default_pii); + .is_some_and(|client| client.options().send_default_pii); - let sentry_req = sentry_request_from_http(&req, with_pii); + let mut sentry_req = sentry_request_from_http(&req, with_pii); let name = transaction_name_from_http(&req); let transaction = if inner.start_transaction { @@ -250,30 +319,41 @@ where "http.server", headers, ); - Some(hub.start_transaction(ctx)) + + let transaction = hub.start_transaction(ctx); + transaction.set_request(sentry_req.clone()); + transaction.set_origin("auto.http.actix"); + Some(transaction) } else { None }; - let parent_span = hub.configure_scope(|scope| { - let parent_span = scope.get_span(); - if let Some(transaction) = transaction.as_ref() { - scope.set_span(Some(transaction.clone().into())); - } else { - scope.set_transaction((!inner.start_transaction).then_some(&name)); + let svc = self.service.clone(); + async move { + let mut req = req; + + if should_capture_request_body(req.headers(), with_pii, max_request_body_size) { + sentry_req.data = Some(capture_request_body(&mut req).await); } - scope.add_event_processor(move |event| Some(process_event(event, &sentry_req))); - parent_span - }); - let fut = self.service.call(req).bind_hub(hub.clone()); + let parent_span = hub.configure_scope(|scope| { + let parent_span = scope.get_span(); + if let Some(transaction) = transaction.as_ref() { + scope.set_span(Some(transaction.clone().into())); + } else { + scope.set_transaction((!inner.start_transaction).then_some(&name)); + } + scope.add_event_processor(move |event| Some(process_event(event, &sentry_req))); + parent_span + }); - async move { - // Service errors + let fut = Hub::run(hub.clone(), || svc.call(req)).bind_hub(hub.clone()); let mut res: Self::Response = match fut.await { Ok(res) => res, Err(e) => { - if inner.capture_server_errors { + // Errors returned by middleware, and possibly other lower level errors + if inner.capture_server_errors && e.error_response().status().is_server_error() + { hub.capture_error(&e); } @@ -350,11 +430,14 @@ fn sentry_request_from_http(request: &ServiceRequest, with_pii: bool) -> Request request.uri() ) .parse() - .ok(), + .ok() + .map(scrub_pii_from_url), method: Some(request.method().to_string()), headers: request .headers() .iter() + .filter(|(_, v)| !v.is_sensitive()) + .filter(|(k, _)| with_pii || !is_sensitive_header(k.as_str())) .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or_default().to_string())) .collect(), ..Default::default() @@ -393,10 +476,12 @@ fn process_event(mut event: Event<'static>, request: &Request) -> Event<'static> mod tests { use std::io; + use actix_web::body::BoxBody; use actix_web::test::{call_service, init_service, TestRequest}; use actix_web::{get, web, App, HttpRequest, HttpResponse}; use futures::executor::block_on; + use futures::future::join_all; use sentry::Level; use super::*; @@ -509,7 +594,7 @@ mod tests { // Current hub should have no events _assert_hub_no_events(); - Err(io::Error::new(io::ErrorKind::Other, "Test Error").into()) + Err(io::Error::other("Test Error").into()) } let app = init_service( @@ -540,9 +625,9 @@ mod tests { } } - /// Ensures client errors (4xx) are not captured. + /// Ensures client errors (4xx) returned by service are not captured. #[actix_web::test] - async fn test_client_errors_discarded() { + async fn test_service_client_errors_discarded() { let events = sentry::test::with_captured_events(|| { block_on(async { let service = HttpResponse::NotFound; @@ -563,6 +648,66 @@ mod tests { assert!(events.is_empty()); } + /// Ensures client errors (4xx) returned by middleware are not captured. + #[actix_web::test] + async fn test_middleware_client_errors_discarded() { + let events = sentry::test::with_captured_events(|| { + block_on(async { + async fn hello_world() -> HttpResponse { + HttpResponse::Ok().body("Hello, world!") + } + + let app = init_service( + App::new() + .wrap_fn(|_, _| async { + Err(actix_web::error::ErrorNotFound("Not found")) + as Result, _> + }) + .wrap(Sentry::builder().with_hub(Hub::current()).finish()) + .service(web::resource("/test").to(hello_world)), + ) + .await; + + let req = TestRequest::get().uri("/test").to_request(); + let res = app.call(req).await; + assert!(res.is_err()); + assert!(res.unwrap_err().error_response().status().is_client_error()); + }) + }); + + assert!(events.is_empty()); + } + + /// Ensures server errors (5xx) returned by middleware are captured. + #[actix_web::test] + async fn test_middleware_server_errors_captured() { + let events = sentry::test::with_captured_events(|| { + block_on(async { + async fn hello_world() -> HttpResponse { + HttpResponse::Ok().body("Hello, world!") + } + + let app = init_service( + App::new() + .wrap_fn(|_, _| async { + Err(actix_web::error::ErrorInternalServerError("Server error")) + as Result, _> + }) + .wrap(Sentry::builder().with_hub(Hub::current()).finish()) + .service(web::resource("/test").to(hello_world)), + ) + .await; + + let req = TestRequest::get().uri("/test").to_request(); + let res = app.call(req).await; + assert!(res.is_err()); + assert!(res.unwrap_err().error_response().status().is_server_error()); + }) + }); + + assert_eq!(events.len(), 1); + } + /// Ensures transaction name can be overridden in handler scope. #[actix_web::test] async fn test_override_transaction_name() { @@ -572,7 +717,7 @@ mod tests { async fn original_transaction(_req: HttpRequest) -> Result { // Override transaction name sentry::configure_scope(|scope| scope.set_transaction(Some("new_transaction"))); - Err(io::Error::new(io::ErrorKind::Other, "Test Error").into()) + Err(io::Error::other("Test Error").into()) } let app = init_service( @@ -599,6 +744,7 @@ mod tests { assert_eq!(request.method, Some("GET".into())); } + #[cfg(feature = "release-health")] #[actix_web::test] async fn test_track_session() { let envelopes = sentry::test::with_captured_envelopes_options( @@ -639,4 +785,51 @@ mod tests { } assert_eq!(items.next(), None); } + + /// Tests that the per-request Hub is used in the handler and both sides of the roundtrip + /// through middleware + #[actix_web::test] + async fn test_middleware_and_handler_use_correct_hub() { + sentry::test::with_captured_events(|| { + block_on(async { + sentry::capture_message("message outside", Level::Error); + + let handler = || { + // an event was captured in the middleware + assert!(Hub::current().last_event_id().is_some()); + sentry::capture_message("second message", Level::Error); + HttpResponse::Ok() + }; + + let app = init_service( + App::new() + .wrap_fn(|req, srv| { + // the event captured outside the per-request Hub is not there + assert!(Hub::current().last_event_id().is_none()); + + let event_id = sentry::capture_message("first message", Level::Error); + + srv.call(req).map(move |res| { + // a different event was captured in the handler + assert!(Hub::current().last_event_id().is_some()); + assert_ne!(Some(event_id), Hub::current().last_event_id()); + res + }) + }) + .wrap(Sentry::builder().with_hub(Hub::current()).finish()) + .service(web::resource("/test").to(handler)), + ) + .await; + + // test with multiple requests in parallel + let mut futures = Vec::new(); + for _ in 0..16 { + let req = TestRequest::get().uri("/test").to_request(); + futures.push(call_service(&app, req)); + } + + join_all(futures).await; + }) + }); + } } diff --git a/sentry-anyhow/Cargo.toml b/sentry-anyhow/Cargo.toml index ac2a44bb0..aa9b87836 100644 --- a/sentry-anyhow/Cargo.toml +++ b/sentry-anyhow/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "sentry-anyhow" -version = "0.32.2" +version = "0.45.0" authors = ["Sentry "] -license = "Apache-2.0" +license = "MIT" readme = "README.md" repository = "https://github.com/getsentry/sentry-rust" homepage = "https://sentry.io/welcome/" @@ -10,16 +10,16 @@ description = """ Sentry integration for anyhow. """ edition = "2021" -rust-version = "1.73" +rust-version = "1.81" [features] default = ["backtrace"] -backtrace = ["anyhow/backtrace"] +backtrace = [] [dependencies] -sentry-backtrace = { version = "0.32.2", path = "../sentry-backtrace" } -sentry-core = { version = "0.32.2", path = "../sentry-core" } -anyhow = "1.0.39" +sentry-backtrace = { version = "0.45.0", path = "../sentry-backtrace" } +sentry-core = { version = "0.45.0", path = "../sentry-core" } +anyhow = "1.0.77" [dev-dependencies] sentry = { path = "../sentry", default-features = false, features = ["test"] } diff --git a/sentry-anyhow/LICENSE b/sentry-anyhow/LICENSE index d97d9399c..3e4988928 100644 --- a/sentry-anyhow/LICENSE +++ b/sentry-anyhow/LICENSE @@ -1,203 +1,21 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 Functional Software, Inc. dba Sentry (https://sentry.io) - and individual contributors. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2021 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sentry-anyhow/README.md b/sentry-anyhow/README.md index 6a8312fb4..4ad56f57e 100644 --- a/sentry-anyhow/README.md +++ b/sentry-anyhow/README.md @@ -46,7 +46,7 @@ capture backtraces with your events. It is enabled by default. ## Resources -License: Apache-2.0 +License: MIT - [Discord](https://discord.gg/ez5KZN7) server for project discussions. - Follow [@getsentry](https://twitter.com/getsentry) on Twitter for updates diff --git a/sentry-anyhow/src/lib.rs b/sentry-anyhow/src/lib.rs index 4127a9567..d5cde23f8 100644 --- a/sentry-anyhow/src/lib.rs +++ b/sentry-anyhow/src/lib.rs @@ -72,7 +72,12 @@ pub fn event_from_error(err: &anyhow::Error) -> Event<'static> { // exception records are sorted in reverse if let Some(exc) = event.exception.iter_mut().last() { let backtrace = err.backtrace(); - exc.stacktrace = sentry_backtrace::parse_stacktrace(&format!("{backtrace:#}")); + if matches!( + backtrace.status(), + std::backtrace::BacktraceStatus::Captured + ) { + exc.stacktrace = sentry_backtrace::parse_stacktrace(&format!("{backtrace:#}")); + } } } diff --git a/sentry-backtrace/Cargo.toml b/sentry-backtrace/Cargo.toml index 8a82b2a3f..b08723ef8 100644 --- a/sentry-backtrace/Cargo.toml +++ b/sentry-backtrace/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "sentry-backtrace" -version = "0.32.2" +version = "0.45.0" authors = ["Sentry "] -license = "Apache-2.0" +license = "MIT" readme = "README.md" repository = "https://github.com/getsentry/sentry-rust" homepage = "https://sentry.io/welcome/" @@ -10,13 +10,12 @@ description = """ Sentry integration and utilities for dealing with stacktraces. """ edition = "2021" -rust-version = "1.73" +rust-version = "1.81" [dependencies] backtrace = "0.3.44" -once_cell = "1" regex = { version = "1.5.5", default-features = false, features = [ "std", "unicode-perl", ] } -sentry-core = { version = "0.32.2", path = "../sentry-core" } +sentry-core = { version = "0.45.0", path = "../sentry-core" } diff --git a/sentry-backtrace/LICENSE b/sentry-backtrace/LICENSE index d97d9399c..3e4988928 100644 --- a/sentry-backtrace/LICENSE +++ b/sentry-backtrace/LICENSE @@ -1,203 +1,21 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 Functional Software, Inc. dba Sentry (https://sentry.io) - and individual contributors. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2021 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sentry-backtrace/README.md b/sentry-backtrace/README.md index 1cbd8340f..ca4e43607 100644 --- a/sentry-backtrace/README.md +++ b/sentry-backtrace/README.md @@ -13,7 +13,7 @@ as integrations to process event stacktraces. ## Resources -License: Apache-2.0 +License: MIT - [Discord](https://discord.gg/ez5KZN7) server for project discussions. - Follow [@getsentry](https://twitter.com/getsentry) on Twitter for updates diff --git a/sentry-backtrace/src/parse.rs b/sentry-backtrace/src/parse.rs index 03f3df892..f2e82fc03 100644 --- a/sentry-backtrace/src/parse.rs +++ b/sentry-backtrace/src/parse.rs @@ -1,10 +1,11 @@ -use once_cell::sync::Lazy; +use std::sync::LazyLock; + use regex::Regex; use crate::utils::{demangle_symbol, filename, strip_symbol}; use crate::{Frame, Stacktrace}; -static FRAME_RE: Lazy = Lazy::new(|| { +static FRAME_RE: LazyLock = LazyLock::new(|| { Regex::new( r#"(?xm) ^ diff --git a/sentry-backtrace/src/process.rs b/sentry-backtrace/src/process.rs index 82334c9a7..90c00335b 100644 --- a/sentry-backtrace/src/process.rs +++ b/sentry-backtrace/src/process.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use backtrace::Backtrace; use sentry_core::ClientOptions; -use crate::trim::{is_sys_function, trim_stacktrace}; +use crate::trim::{is_well_known_not_in_app, trim_stacktrace}; use crate::utils::{ demangle_symbol, filename, function_starts_with, parse_crate_name, strip_symbol, }; @@ -72,7 +72,7 @@ pub fn process_event_stacktrace(stacktrace: &mut Stacktrace, options: &ClientOpt continue; } - if is_sys_function(func_name) { + if is_well_known_not_in_app(func_name) { frame.in_app = Some(false); } } diff --git a/sentry-backtrace/src/trim.rs b/sentry-backtrace/src/trim.rs index d74150054..ec1087cc6 100644 --- a/sentry-backtrace/src/trim.rs +++ b/sentry-backtrace/src/trim.rs @@ -2,7 +2,8 @@ use sentry_core::protocol::{Frame, Stacktrace}; use crate::utils::function_starts_with; -const WELL_KNOWN_SYS_MODULES: &[&str] = &[ +const WELL_KNOWN_NOT_IN_APP: &[&str] = &[ + // standard library and sentry crates "std::", "core::", "alloc::", @@ -10,14 +11,19 @@ const WELL_KNOWN_SYS_MODULES: &[&str] = &[ "sentry::", "sentry_core::", "sentry_types::", + "sentry_backtrace::", // these are not modules but things like __rust_maybe_catch_panic "__rust_", "___rust_", + "rust_begin_unwind", + "_start", // these are well-known library frames "anyhow::", "log::", "tokio::", "tracing_core::", + "futures_core::", + "futures_util::", ]; const WELL_KNOWN_BORDER_FRAMES: &[&str] = &[ @@ -39,7 +45,7 @@ where .iter() .rev() .position(|frame| match frame.function { - Some(ref func) => is_well_known(func) || f(frame, stacktrace), + Some(ref func) => is_well_known_border_frame(func) || f(frame, stacktrace), None => false, }); @@ -49,15 +55,15 @@ where } } -/// Checks if a function is considered to be not in-app -pub fn is_sys_function(func: &str) -> bool { - WELL_KNOWN_SYS_MODULES +/// Checks if a function is from a module that shall be considered not in-app by default +pub fn is_well_known_not_in_app(func: &str) -> bool { + WELL_KNOWN_NOT_IN_APP .iter() .any(|m| function_starts_with(func, m)) } -/// Checks if a function is a well-known system function -fn is_well_known(func: &str) -> bool { +/// Checks if a function is a well-known border frame +fn is_well_known_border_frame(func: &str) -> bool { WELL_KNOWN_BORDER_FRAMES .iter() .any(|m| function_starts_with(func, m)) diff --git a/sentry-backtrace/src/utils.rs b/sentry-backtrace/src/utils.rs index 6b018ff50..1ce0750e0 100644 --- a/sentry-backtrace/src/utils.rs +++ b/sentry-backtrace/src/utils.rs @@ -1,9 +1,8 @@ -use std::borrow::Cow; +use std::{borrow::Cow, sync::LazyLock}; -use once_cell::sync::Lazy; use regex::{Captures, Regex}; -static HASH_FUNC_RE: Lazy = Lazy::new(|| { +static HASH_FUNC_RE: LazyLock = LazyLock::new(|| { Regex::new( r#"(?x) ^(.*)::h[a-f0-9]{16}$ @@ -12,7 +11,7 @@ static HASH_FUNC_RE: Lazy = Lazy::new(|| { .unwrap() }); -static CRATE_HASH_RE: Lazy = Lazy::new(|| { +static CRATE_HASH_RE: LazyLock = LazyLock::new(|| { Regex::new( r"(?x) \b(\[[a-f0-9]{16}\]) @@ -21,7 +20,7 @@ static CRATE_HASH_RE: Lazy = Lazy::new(|| { .unwrap() }); -static CRATE_RE: Lazy = Lazy::new(|| { +static CRATE_RE: LazyLock = LazyLock::new(|| { Regex::new( r"(?x) ^ @@ -34,7 +33,7 @@ static CRATE_RE: Lazy = Lazy::new(|| { .unwrap() }); -static COMMON_RUST_SYMBOL_ESCAPES_RE: Lazy = Lazy::new(|| { +static COMMON_RUST_SYMBOL_ESCAPES_RE: LazyLock = LazyLock::new(|| { Regex::new( r"(?x) \$ @@ -58,7 +57,7 @@ pub fn filename(s: &str) -> &str { s.rsplit(&['/', '\\'][..]).next().unwrap() } -pub fn strip_symbol(s: &str) -> Cow { +pub fn strip_symbol(s: &str) -> Cow<'_, str> { let stripped_trailing_hash = HASH_FUNC_RE .captures(s) .map(|c| c.get(1).unwrap().as_str()) @@ -97,12 +96,15 @@ pub fn demangle_symbol(s: &str) -> String { /// /// In trait implementations, the original type name is wrapped in "_< ... >" and colons are /// replaced with dots. This function accounts for differences while checking. +/// The ` bool { if pattern.starts_with('<') { while pattern.starts_with('<') { pattern = &pattern[1..]; - if func_name.starts_with('<') { + if func_name.starts_with(" bool { } } } else { - func_name = func_name.trim_start_matches('<').trim_start_matches("_<"); + func_name = func_name + .trim_start_matches(""] -license = "Apache-2.0" +license = "MIT" readme = "README.md" repository = "https://github.com/getsentry/sentry-rust" homepage = "https://sentry.io/welcome/" @@ -11,12 +11,12 @@ Sentry integration for os, device, and rust contexts. """ build = "build.rs" edition = "2021" -rust-version = "1.73" +rust-version = "1.81" [dependencies] -sentry-core = { version = "0.32.2", path = "../sentry-core" } +sentry-core = { version = "0.45.0", path = "../sentry-core" } libc = "0.2.66" -hostname = "0.3.0" +hostname = "0.4" [target."cfg(not(windows))".dependencies] uname = "0.1.1" diff --git a/sentry-contexts/LICENSE b/sentry-contexts/LICENSE index d97d9399c..3e4988928 100644 --- a/sentry-contexts/LICENSE +++ b/sentry-contexts/LICENSE @@ -1,203 +1,21 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 Functional Software, Inc. dba Sentry (https://sentry.io) - and individual contributors. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2021 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sentry-contexts/README.md b/sentry-contexts/README.md index 31a85585b..f2abcbb78 100644 --- a/sentry-contexts/README.md +++ b/sentry-contexts/README.md @@ -25,7 +25,7 @@ let _sentry = sentry::init(sentry::ClientOptions::new().add_integration(integrat ## Resources -License: Apache-2.0 +License: MIT - [Discord](https://discord.gg/ez5KZN7) server for project discussions. - Follow [@getsentry](https://twitter.com/getsentry) on Twitter for updates diff --git a/sentry-core/Cargo.toml b/sentry-core/Cargo.toml index c1bbf1846..da21ae27e 100644 --- a/sentry-core/Cargo.toml +++ b/sentry-core/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "sentry-core" -version = "0.32.2" +version = "0.45.0" authors = ["Sentry "] -license = "Apache-2.0" +license = "MIT" readme = "README.md" repository = "https://github.com/getsentry/sentry-rust" homepage = "https://sentry.io/welcome/" @@ -10,7 +10,7 @@ description = """ Core sentry library used for instrumentation and integration development. """ edition = "2021" -rust-version = "1.73" +rust-version = "1.81" [package.metadata.docs.rs] all-features = true @@ -22,21 +22,17 @@ harness = false [features] default = [] client = ["rand"] -# I would love to just have a `log` feature, but this is used inside a macro, -# and macros actually expand features (and extern crate) where they are used! -debug-logs = ["dep:log"] -test = ["client"] -UNSTABLE_metrics = ["sentry-types/UNSTABLE_metrics"] -UNSTABLE_cadence = ["dep:cadence", "UNSTABLE_metrics"] +test = ["client", "release-health"] +release-health = [] +logs = [] [dependencies] -cadence = { version = "0.29.0", optional = true } log = { version = "0.4.8", optional = true, features = ["std"] } -once_cell = "1" -rand = { version = "0.8.1", optional = true } -sentry-types = { version = "0.32.2", path = "../sentry-types" } +rand = { version = "0.9.0", optional = true } +sentry-types = { version = "0.45.0", path = "../sentry-types" } serde = { version = "1.0.104", features = ["derive"] } serde_json = { version = "1.0.46" } +url = { version = "2.1.1" } uuid = { version = "1.0.0", features = ["v4", "serde"], optional = true } [dev-dependencies] @@ -51,5 +47,5 @@ anyhow = "1.0.30" criterion = "0.5" futures = "0.3.24" rayon = "1.5.3" -thiserror = "1.0.15" -tokio = { version = "1.0", features = ["rt", "rt-multi-thread", "macros"] } +thiserror = "2.0.12" +tokio = { version = "1.44", features = ["rt", "rt-multi-thread", "macros"] } diff --git a/sentry-core/LICENSE b/sentry-core/LICENSE index d97d9399c..3e4988928 100644 --- a/sentry-core/LICENSE +++ b/sentry-core/LICENSE @@ -1,203 +1,21 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 Functional Software, Inc. dba Sentry (https://sentry.io) - and individual contributors. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2021 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sentry-core/README.md b/sentry-core/README.md index 498e54256..41aab7d9f 100644 --- a/sentry-core/README.md +++ b/sentry-core/README.md @@ -95,18 +95,15 @@ functionality. - `feature = "test"`: Activates the [`test`] module, which can be used to write integration tests. It comes with a test transport which can capture all sent events for inspection. -- `feature = "debug-logs"`: Uses the `log` crate for debug output, instead - of printing to `stderr`. This feature is **deprecated** and will be - replaced by a dedicated log callback in the future. [Sentry]: https://sentry.io/ [`sentry`]: https://crates.io/crates/sentry [Unified API]: https://develop.sentry.dev/sdk/unified-api/ -[`test`]: https://docs.rs/sentry-core/0.32.2/sentry_core/test/index.html +[`test`]: https://docs.rs/sentry-core/0.45.0/sentry_core/test/index.html ## Resources -License: Apache-2.0 +License: MIT - [Discord](https://discord.gg/ez5KZN7) server for project discussions. - Follow [@getsentry](https://twitter.com/getsentry) on Twitter for updates diff --git a/sentry-core/src/api.rs b/sentry-core/src/api.rs index 06164e4e3..ea0b8532f 100644 --- a/sentry-core/src/api.rs +++ b/sentry-core/src/api.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "release-health")] use sentry_types::protocol::v7::SessionStatus; use crate::protocol::{Event, Level}; @@ -279,11 +280,13 @@ pub fn last_event_id() -> Option { /// /// sentry::end_session(); /// ``` +#[cfg(feature = "release-health")] pub fn start_session() { Hub::with_active(|hub| hub.start_session()) } /// End the current Release Health Session. +#[cfg(feature = "release-health")] pub fn end_session() { end_session_with_status(SessionStatus::Exited) } @@ -295,6 +298,7 @@ pub fn end_session() { /// /// When an `Abnormal` session should be captured, it has to be done explicitly /// using this function. +#[cfg(feature = "release-health")] pub fn end_session_with_status(status: SessionStatus) { Hub::with_active(|hub| hub.end_session_with_status(status)) } diff --git a/sentry-core/src/cadence.rs b/sentry-core/src/cadence.rs deleted file mode 100644 index 8cd7c3d0d..000000000 --- a/sentry-core/src/cadence.rs +++ /dev/null @@ -1,162 +0,0 @@ -//! [`cadence`] integration for Sentry. -//! -//! [`cadence`] is a popular Statsd client for Rust. The [`SentryMetricSink`] provides a drop-in -//! integration to send metrics captured via `cadence` to Sentry. For direct usage of Sentry -//! metrics, see the [`metrics`](crate::metrics) module. -//! -//! # Usage -//! -//! To use the `cadence` integration, enable the `UNSTABLE_cadence` feature in your `Cargo.toml`. -//! Then, create a [`SentryMetricSink`] and pass it to your `cadence` client: -//! -//! ``` -//! use cadence::StatsdClient; -//! use sentry::cadence::SentryMetricSink; -//! -//! let client = StatsdClient::from_sink("sentry.test", SentryMetricSink::new()); -//! ``` -//! -//! # Side-by-side Usage -//! -//! If you want to send metrics to Sentry and another backend at the same time, you can use -//! [`SentryMetricSink::wrap`] to wrap another [`MetricSink`]: -//! -//! ``` -//! use cadence::{StatsdClient, NopMetricSink}; -//! use sentry::cadence::SentryMetricSink; -//! -//! let sink = SentryMetricSink::wrap(NopMetricSink); -//! let client = StatsdClient::from_sink("sentry.test", sink); -//! ``` - -use std::sync::Arc; - -use cadence::{MetricSink, NopMetricSink}; - -use crate::metrics::Metric; -use crate::{Client, Hub}; - -/// A [`MetricSink`] that sends metrics to Sentry. -/// -/// This metric sends all metrics to Sentry. The Sentry client is internally buffered, so submission -/// will be delayed. -/// -/// Optionally, this sink can also forward metrics to another [`MetricSink`]. This is useful if you -/// want to send metrics to Sentry and another backend at the same time. Use -/// [`SentryMetricSink::wrap`] to construct such a sink. -#[derive(Debug)] -pub struct SentryMetricSink { - client: Option>, - sink: S, -} - -impl SentryMetricSink -where - S: MetricSink, -{ - /// Creates a new [`SentryMetricSink`], wrapping the given [`MetricSink`]. - pub fn wrap(sink: S) -> Self { - Self { client: None, sink } - } - - /// Creates a new [`SentryMetricSink`] sending data to the given [`Client`]. - pub fn with_client(mut self, client: Arc) -> Self { - self.client = Some(client); - self - } -} - -impl SentryMetricSink { - /// Creates a new [`SentryMetricSink`]. - /// - /// It is not required that a client is available when this sink is created. The sink sends - /// metrics to the client of the Sentry hub that is registered when the metrics are emitted. - pub fn new() -> Self { - Self { - client: None, - sink: NopMetricSink, - } - } -} - -impl Default for SentryMetricSink { - fn default() -> Self { - Self::new() - } -} - -impl MetricSink for SentryMetricSink { - fn emit(&self, string: &str) -> std::io::Result { - if let Ok(metric) = Metric::parse_statsd(string) { - if let Some(ref client) = self.client { - client.add_metric(metric); - } else if let Some(client) = Hub::current().client() { - client.add_metric(metric); - } - } - - // NopMetricSink returns `0`, which is correct as Sentry is buffering the metrics. - self.sink.emit(string) - } - - fn flush(&self) -> std::io::Result<()> { - let flushed = if let Some(ref client) = self.client { - client.flush(None) - } else if let Some(client) = Hub::current().client() { - client.flush(None) - } else { - true - }; - - let sink_result = self.sink.flush(); - - if !flushed { - Err(std::io::Error::new( - std::io::ErrorKind::Other, - "failed to flush metrics to Sentry", - )) - } else { - sink_result - } - } -} - -#[cfg(test)] -mod tests { - use cadence::{Counted, Distributed}; - use sentry_types::protocol::latest::EnvelopeItem; - - use crate::test::with_captured_envelopes; - - use super::*; - - #[test] - fn test_basic_metrics() { - let envelopes = with_captured_envelopes(|| { - let client = cadence::StatsdClient::from_sink("sentry.test", SentryMetricSink::new()); - client.count("some.count", 1).unwrap(); - client.count("some.count", 10).unwrap(); - client - .count_with_tags("count.with.tags", 1) - .with_tag("foo", "bar") - .send(); - client.distribution("some.distr", 1).unwrap(); - client.distribution("some.distr", 2).unwrap(); - client.distribution("some.distr", 3).unwrap(); - }); - assert_eq!(envelopes.len(), 1); - - let mut items = envelopes[0].items(); - let Some(EnvelopeItem::Statsd(metrics)) = items.next() else { - panic!("expected metrics"); - }; - let metrics = std::str::from_utf8(metrics).unwrap(); - - println!("{metrics}"); - - assert!(metrics.contains("sentry.test.count.with.tags:1|c|#foo:bar|T")); - assert!(metrics.contains("sentry.test.some.count:11|c|T")); - assert!(metrics.contains("sentry.test.some.distr:1:2:3|d|T")); - assert_eq!(items.next(), None); - } -} diff --git a/sentry-core/src/client.rs b/sentry-core/src/client.rs index a485d99ca..a4b72d080 100644 --- a/sentry-core/src/client.rs +++ b/sentry-core/src/client.rs @@ -1,22 +1,31 @@ use std::any::TypeId; use std::borrow::Cow; +#[cfg(feature = "logs")] +use std::collections::BTreeMap; use std::fmt; use std::panic::RefUnwindSafe; -use std::sync::Arc; -use std::sync::RwLock; +use std::sync::{Arc, RwLock}; use std::time::Duration; +#[cfg(feature = "release-health")] +use crate::protocol::SessionUpdate; use rand::random; -use sentry_types::protocol::v7::SessionUpdate; use sentry_types::random_uuid; use crate::constants::SDK_INFO; -#[cfg(feature = "UNSTABLE_metrics")] -use crate::metrics::{self, MetricAggregator}; +#[cfg(feature = "logs")] +use crate::logs::LogsBatcher; use crate::protocol::{ClientSdkInfo, Event}; +#[cfg(feature = "release-health")] use crate::session::SessionFlusher; use crate::types::{Dsn, Uuid}; -use crate::{ClientOptions, Envelope, Hub, Integration, Scope, SessionMode, Transport}; +#[cfg(feature = "release-health")] +use crate::SessionMode; +use crate::{ClientOptions, Envelope, Hub, Integration, Scope, Transport}; +#[cfg(feature = "logs")] +use sentry_types::protocol::v7::Context; +#[cfg(feature = "logs")] +use sentry_types::protocol::v7::{Log, LogAttribute}; impl> From for Client { fn from(o: T) -> Client { @@ -46,9 +55,12 @@ pub(crate) type TransportArc = Arc>>>; pub struct Client { options: ClientOptions, transport: TransportArc, + #[cfg(feature = "release-health")] session_flusher: RwLock>, - #[cfg(feature = "UNSTABLE_metrics")] - metric_aggregator: RwLock>, + #[cfg(feature = "logs")] + logs_batcher: RwLock>, + #[cfg(feature = "logs")] + default_log_attributes: Option>, integrations: Vec<(TypeId, Arc)>, pub(crate) sdk_info: ClientSdkInfo, } @@ -65,21 +77,29 @@ impl fmt::Debug for Client { impl Clone for Client { fn clone(&self) -> Client { let transport = Arc::new(RwLock::new(self.transport.read().unwrap().clone())); + + #[cfg(feature = "release-health")] let session_flusher = RwLock::new(Some(SessionFlusher::new( transport.clone(), self.options.session_mode, ))); - #[cfg(feature = "UNSTABLE_metrics")] - let metric_aggregator = RwLock::new(Some(MetricAggregator::new( - transport.clone(), - &self.options, - ))); + + #[cfg(feature = "logs")] + let logs_batcher = RwLock::new(if self.options.enable_logs { + Some(LogsBatcher::new(transport.clone())) + } else { + None + }); + Client { options: self.options.clone(), transport, + #[cfg(feature = "release-health")] session_flusher, - #[cfg(feature = "UNSTABLE_metrics")] - metric_aggregator, + #[cfg(feature = "logs")] + logs_batcher, + #[cfg(feature = "logs")] + default_log_attributes: self.default_log_attributes.clone(), integrations: self.integrations.clone(), sdk_info: self.sdk_info.clone(), } @@ -143,24 +163,88 @@ impl Client { sdk_info.integrations.push(integration.name().to_string()); } + #[cfg(feature = "release-health")] let session_flusher = RwLock::new(Some(SessionFlusher::new( transport.clone(), options.session_mode, ))); - #[cfg(feature = "UNSTABLE_metrics")] - let metric_aggregator = - RwLock::new(Some(MetricAggregator::new(transport.clone(), &options))); + #[cfg(feature = "logs")] + let logs_batcher = RwLock::new(if options.enable_logs { + Some(LogsBatcher::new(transport.clone())) + } else { + None + }); - Client { + #[allow(unused_mut)] + let mut client = Client { options, transport, + #[cfg(feature = "release-health")] session_flusher, - #[cfg(feature = "UNSTABLE_metrics")] - metric_aggregator, + #[cfg(feature = "logs")] + logs_batcher, + #[cfg(feature = "logs")] + default_log_attributes: None, integrations, sdk_info, + }; + + #[cfg(feature = "logs")] + client.cache_default_log_attributes(); + + client + } + + #[cfg(feature = "logs")] + fn cache_default_log_attributes(&mut self) { + let mut attributes = BTreeMap::new(); + + if let Some(environment) = self.options.environment.as_ref() { + attributes.insert("sentry.environment".to_owned(), environment.clone().into()); + } + + if let Some(release) = self.options.release.as_ref() { + attributes.insert("sentry.release".to_owned(), release.clone().into()); + } + + attributes.insert( + "sentry.sdk.name".to_owned(), + self.sdk_info.name.to_owned().into(), + ); + + attributes.insert( + "sentry.sdk.version".to_owned(), + self.sdk_info.version.to_owned().into(), + ); + + // Process a fake event through integrations, so that `ContextIntegration` (if available) + // provides the OS Context. + // This is needed as that integration adds the OS Context to events using an event + // processor, which logs don't go through. + // We cannot get the `ContextIntegration` directly, as its type lives in `sentry-contexts`, + // which `sentry-core` doesn't depend on. + let mut fake_event = Event::default(); + for (_, integration) in self.integrations.iter() { + if let Some(res) = integration.process_event(fake_event.clone(), &self.options) { + fake_event = res; + } + } + + if let Some(Context::Os(os)) = fake_event.contexts.get("os") { + if let Some(name) = os.name.as_ref() { + attributes.insert("os.name".to_owned(), name.to_owned().into()); + } + if let Some(version) = os.version.as_ref() { + attributes.insert("os.version".to_owned(), version.to_owned().into()); + } } + + if let Some(server) = &self.options.server_name { + attributes.insert("server.address".to_owned(), server.clone().into()); + } + + self.default_log_attributes = Some(attributes); } pub(crate) fn get_integration(&self) -> Option<&I> @@ -172,7 +256,8 @@ impl Client { integration.as_ref().as_any().downcast_ref() } - fn prepare_event( + /// Prepares an event for transmission to sentry. + pub fn prepare_event( &self, mut event: Event<'static>, scope: Option<&Scope>, @@ -189,10 +274,7 @@ impl Client { } if let Some(scope) = scope { - event = match scope.apply_to_event(event) { - Some(event) => event, - None => return None, - }; + event = scope.apply_to_event(event)?; } for (_, integration) in self.integrations.iter() { @@ -207,13 +289,13 @@ impl Client { } if event.release.is_none() { - event.release = self.options.release.clone(); + event.release.clone_from(&self.options.release); } if event.environment.is_none() { - event.environment = self.options.environment.clone(); + event.environment.clone_from(&self.options.environment); } if event.server_name.is_none() { - event.server_name = self.options.server_name.clone(); + event.server_name.clone_from(&self.options.server_name); } if &event.platform == "other" { event.platform = "native".into(); @@ -286,6 +368,7 @@ impl Client { let mut envelope: Envelope = event.into(); // For request-mode sessions, we aggregate them all instead of // flushing them out early. + #[cfg(feature = "release-health")] if self.options.session_mode == SessionMode::Application { let session_item = scope.and_then(|scope| { scope @@ -320,28 +403,22 @@ impl Client { } } + #[cfg(feature = "release-health")] pub(crate) fn enqueue_session(&self, session_update: SessionUpdate<'static>) { if let Some(ref flusher) = *self.session_flusher.read().unwrap() { flusher.enqueue(session_update); } } - /// Captures a metric and sends it to Sentry on the next flush. - #[cfg(feature = "UNSTABLE_metrics")] - pub fn add_metric(&self, metric: metrics::Metric) { - if let Some(ref aggregator) = *self.metric_aggregator.read().unwrap() { - aggregator.add(metric) - } - } - /// Drains all pending events without shutting down. pub fn flush(&self, timeout: Option) -> bool { + #[cfg(feature = "release-health")] if let Some(ref flusher) = *self.session_flusher.read().unwrap() { flusher.flush(); } - #[cfg(feature = "UNSTABLE_metrics")] - if let Some(ref aggregator) = *self.metric_aggregator.read().unwrap() { - aggregator.flush(); + #[cfg(feature = "logs")] + if let Some(ref batcher) = *self.logs_batcher.read().unwrap() { + batcher.flush(); } if let Some(ref transport) = *self.transport.read().unwrap() { transport.flush(timeout.unwrap_or(self.options.shutdown_timeout)) @@ -358,9 +435,10 @@ impl Client { /// If no timeout is provided the client will wait for as long a /// `shutdown_timeout` in the client options. pub fn close(&self, timeout: Option) -> bool { + #[cfg(feature = "release-health")] drop(self.session_flusher.write().unwrap().take()); - #[cfg(feature = "UNSTABLE_metrics")] - drop(self.metric_aggregator.write().unwrap().take()); + #[cfg(feature = "logs")] + drop(self.logs_batcher.write().unwrap().take()); let transport_opt = self.transport.write().unwrap().take(); if let Some(transport) = transport_opt { sentry_debug!("client close; request transport to shut down"); @@ -376,10 +454,45 @@ impl Client { pub fn sample_should_send(&self, rate: f32) -> bool { if rate >= 1.0 { true + } else if rate <= 0.0 { + false } else { random::() < rate } } + + /// Captures a log and sends it to Sentry. + #[cfg(feature = "logs")] + pub fn capture_log(&self, log: Log, scope: &Scope) { + if !self.options.enable_logs { + sentry_debug!("[Client] called capture_log, but options.enable_logs is set to false"); + return; + } + if let Some(log) = self.prepare_log(log, scope) { + if let Some(ref batcher) = *self.logs_batcher.read().unwrap() { + batcher.enqueue(log); + } + } + } + + /// Prepares a log to be sent, setting the `trace_id` and other default attributes, and + /// processing it through `before_send_log`. + #[cfg(feature = "logs")] + fn prepare_log(&self, mut log: Log, scope: &Scope) -> Option { + scope.apply_to_log(&mut log); + + if let Some(default_attributes) = self.default_log_attributes.as_ref() { + for (key, val) in default_attributes.iter() { + log.attributes.entry(key.to_owned()).or_insert(val.clone()); + } + } + + if let Some(ref func) = self.options.before_send_log { + log = func(log)?; + } + + Some(log) + } } // Make this unwind safe. It's not out of the box because of the diff --git a/sentry-core/src/clientoptions.rs b/sentry-core/src/clientoptions.rs index 9926cbcdd..33b5bbd4c 100644 --- a/sentry-core/src/clientoptions.rs +++ b/sentry-core/src/clientoptions.rs @@ -5,6 +5,8 @@ use std::time::Duration; use crate::constants::USER_AGENT; use crate::performance::TracesSampler; +#[cfg(feature = "logs")] +use crate::protocol::Log; use crate::protocol::{Breadcrumb, Event}; use crate::types::Dsn; use crate::{Integration, IntoDsn, TransportFactory}; @@ -32,6 +34,9 @@ pub type BeforeCallback = Arc Option + Send + Sync>; /// /// See the [Documentation on Session Modes](https://develop.sentry.dev/sdk/sessions/#sdk-considerations) /// for more information. +/// +/// **NOTE**: The `release-health` feature (enabled by default) needs to be enabled for this option to have +/// any effect. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum SessionMode { /// Long running application session. @@ -40,6 +45,39 @@ pub enum SessionMode { Request, } +/// The maximum size of an HTTP request body that the SDK captures. +/// +/// Only request bodies that parse as JSON or form data are currently captured. +/// See the [Documentation on attaching request body](https://develop.sentry.dev/sdk/expected-features/#attaching-request-body-in-server-sdks) +/// and the [Documentation on handling sensitive data](https://develop.sentry.dev/sdk/expected-features/data-handling/#sensitive-data) +/// for more information. +#[derive(Clone, Copy, PartialEq)] +pub enum MaxRequestBodySize { + /// Don't capture request body + None, + /// Capture up to 1000 bytes + Small, + /// Capture up to 10000 bytes + Medium, + /// Capture entire body + Always, + /// Capture up to a specific size + Explicit(usize), +} + +impl MaxRequestBodySize { + /// Check if the content length is within the size limit. + pub fn is_within_size_limit(&self, content_length: usize) -> bool { + match self { + MaxRequestBodySize::None => false, + MaxRequestBodySize::Small => content_length <= 1_000, + MaxRequestBodySize::Medium => content_length <= 10_000, + MaxRequestBodySize::Always => true, + MaxRequestBodySize::Explicit(size) => content_length <= *size, + } + } +} + /// Configuration settings for the client. /// /// These options are explained in more detail in the general @@ -61,8 +99,7 @@ pub struct ClientOptions { /// Enables debug mode. /// /// In debug mode debug information is printed to stderr to help you understand what - /// sentry is doing. When the `log` feature is enabled, Sentry will instead - /// log to the `sentry` logger independently of this flag with the `Debug` level. + /// sentry is doing. pub debug: bool, /// The release to be sent with events. pub release: Option>, @@ -84,7 +121,7 @@ pub struct ClientOptions { pub max_breadcrumbs: usize, /// Attaches stacktraces to messages. pub attach_stacktrace: bool, - /// If turned on some default PII informat is attached. + /// If turned on, some information that can be considered PII is captured, such as potentially sensitive HTTP headers and user IP address in HTTP server integrations. pub send_default_pii: bool, /// The server name to be reported. pub server_name: Option>, @@ -108,6 +145,9 @@ pub struct ClientOptions { pub before_send: Option>>, /// Callback that is executed for each Breadcrumb being added. pub before_breadcrumb: Option>, + /// Callback that is executed for each Log being added. + #[cfg(feature = "logs")] + pub before_send_log: Option>, // Transport options /// The transport to use. /// @@ -126,6 +166,12 @@ pub struct ClientOptions { pub https_proxy: Option>, /// The timeout on client drop for draining events on shutdown. pub shutdown_timeout: Duration, + /// Controls the maximum size of an HTTP request body that can be captured when using HTTP + /// server integrations. Needs `send_default_pii` to be enabled to have any effect. + pub max_request_body_size: MaxRequestBodySize, + /// Determines whether captured structured logs should be sent to Sentry (defaults to false). + #[cfg(feature = "logs")] + pub enable_logs: bool, // Other options not documented in Unified API /// Disable SSL verification. /// @@ -138,8 +184,10 @@ pub struct ClientOptions { /// When automatic session tracking is enabled, a new "user-mode" session /// is started at the time of `sentry::init`, and will persist for the /// application lifetime. + #[cfg(feature = "release-health")] pub auto_session_tracking: bool, /// Determine how Sessions are being tracked. + #[cfg(feature = "release-health")] pub session_mode: SessionMode, /// Border frames which indicate a border from a backtrace to /// useless internals. Some are automatically included. @@ -183,12 +231,19 @@ impl fmt::Debug for ClientOptions { #[derive(Debug)] struct BeforeBreadcrumb; let before_breadcrumb = self.before_breadcrumb.as_ref().map(|_| BeforeBreadcrumb); + #[cfg(feature = "logs")] + let before_send_log = { + #[derive(Debug)] + struct BeforeSendLog; + self.before_send_log.as_ref().map(|_| BeforeSendLog) + }; #[derive(Debug)] struct TransportFactory; let integrations: Vec<_> = self.integrations.iter().map(|i| i.name()).collect(); - f.debug_struct("ClientOptions") + let mut debug_struct = f.debug_struct("ClientOptions"); + debug_struct .field("dsn", &self.dsn) .field("debug", &self.debug) .field("release", &self.release) @@ -216,9 +271,19 @@ impl fmt::Debug for ClientOptions { .field("http_proxy", &self.http_proxy) .field("https_proxy", &self.https_proxy) .field("shutdown_timeout", &self.shutdown_timeout) - .field("accept_invalid_certs", &self.accept_invalid_certs) + .field("accept_invalid_certs", &self.accept_invalid_certs); + + #[cfg(feature = "release-health")] + debug_struct .field("auto_session_tracking", &self.auto_session_tracking) - .field("session_mode", &self.session_mode) + .field("session_mode", &self.session_mode); + + #[cfg(feature = "logs")] + debug_struct + .field("enable_logs", &self.enable_logs) + .field("before_send_log", &before_send_log); + + debug_struct .field("extra_border_frames", &self.extra_border_frames) .field("trim_backtraces", &self.trim_backtraces) .field("user_agent", &self.user_agent) @@ -251,11 +316,18 @@ impl Default for ClientOptions { https_proxy: None, shutdown_timeout: Duration::from_secs(2), accept_invalid_certs: false, + #[cfg(feature = "release-health")] auto_session_tracking: false, + #[cfg(feature = "release-health")] session_mode: SessionMode::Application, extra_border_frames: vec![], trim_backtraces: true, user_agent: Cow::Borrowed(USER_AGENT), + max_request_body_size: MaxRequestBodySize::Medium, + #[cfg(feature = "logs")] + enable_logs: true, + #[cfg(feature = "logs")] + before_send_log: None, } } } diff --git a/sentry-core/src/constants.rs b/sentry-core/src/constants.rs index f33a606b4..d2ce87fb2 100644 --- a/sentry-core/src/constants.rs +++ b/sentry-core/src/constants.rs @@ -1,7 +1,7 @@ // Not all constants are used when building without the "client" feature #![allow(dead_code)] -use once_cell::sync::Lazy; +use std::sync::LazyLock; use crate::protocol::{ClientSdkInfo, ClientSdkPackage}; @@ -9,7 +9,7 @@ use crate::protocol::{ClientSdkInfo, ClientSdkPackage}; const VERSION: &str = env!("CARGO_PKG_VERSION"); pub(crate) const USER_AGENT: &str = concat!("sentry.rust/", env!("CARGO_PKG_VERSION")); -pub(crate) static SDK_INFO: Lazy = Lazy::new(|| ClientSdkInfo { +pub(crate) static SDK_INFO: LazyLock = LazyLock::new(|| ClientSdkInfo { name: "sentry.rust".into(), version: VERSION.into(), packages: vec![ClientSdkPackage { diff --git a/sentry-core/src/hub.rs b/sentry-core/src/hub.rs index cc0a9c3d1..ebd70b0ca 100644 --- a/sentry-core/src/hub.rs +++ b/sentry-core/src/hub.rs @@ -4,11 +4,11 @@ use std::sync::{Arc, RwLock}; -use crate::protocol::{Event, Level, SessionStatus}; +use crate::protocol::{Event, Level, Log, LogAttribute, LogLevel, Map, SessionStatus}; use crate::types::Uuid; use crate::{Integration, IntoBreadcrumbs, Scope, ScopeGuard}; -/// The central object that can manages scopes and clients. +/// The central object that can manage scopes and clients. /// /// This can be used to capture events and manage the scope. This object is [`Send`] and /// [`Sync`] so it can be used from multiple threads if needed. @@ -16,10 +16,10 @@ use crate::{Integration, IntoBreadcrumbs, Scope, ScopeGuard}; /// Each thread has its own thread-local ( see [`Hub::current`]) hub, which is /// automatically derived from the main hub ([`Hub::main`]). /// -/// In most situations developers do not need to interface with the hub directly. Instead -/// toplevel convenience functions are expose that will automatically dispatch -/// to the thread-local ([`Hub::current`]) hub. In some situations this might not be -/// possible in which case it might become necessary to manually work with the +/// In most situations, developers do not need to interface with the hub directly. Instead +/// toplevel convenience functions are exposed that will automatically dispatch +/// to the thread-local ([`Hub::current`]) hub. In some situations, this might not be +/// possible, in which case it might become necessary to manually work with the /// hub. See the main [`crate`] docs for some common use-cases and pitfalls /// related to parallel, concurrent or async code. /// @@ -126,6 +126,7 @@ impl Hub { /// /// See the global [`start_session`](fn.start_session.html) /// for more documentation. + #[cfg(feature = "release-health")] pub fn start_session(&self) { with_client_impl! {{ self.inner.with_mut(|stack| { @@ -143,6 +144,7 @@ impl Hub { /// End the current Release Health Session. /// /// See the global [`sentry::end_session`](crate::end_session) for more documentation. + #[cfg(feature = "release-health")] pub fn end_session(&self) { self.end_session_with_status(SessionStatus::Exited) } @@ -151,6 +153,7 @@ impl Hub { /// /// See the global [`end_session_with_status`](crate::end_session_with_status) /// for more documentation. + #[cfg(feature = "release-health")] pub fn end_session_with_status(&self, status: SessionStatus) { with_client_impl! {{ self.inner.with_mut(|stack| { @@ -242,4 +245,14 @@ impl Hub { }) }} } + + /// Captures a structured log. + #[cfg(feature = "logs")] + pub fn capture_log(&self, log: Log) { + with_client_impl! {{ + let top = self.inner.with(|stack| stack.top().clone()); + let Some(ref client) = top.client else { return }; + client.capture_log(log, &top.scope); + }} + } } diff --git a/sentry-core/src/hub_impl.rs b/sentry-core/src/hub_impl.rs index 76a91c3d9..5786daa36 100644 --- a/sentry-core/src/hub_impl.rs +++ b/sentry-core/src/hub_impl.rs @@ -1,13 +1,11 @@ use std::cell::{Cell, UnsafeCell}; -use std::sync::{Arc, PoisonError, RwLock}; +use std::sync::{Arc, LazyLock, PoisonError, RwLock}; use std::thread; use crate::Scope; use crate::{scope::Stack, Client, Hub}; -use once_cell::sync::Lazy; - -static PROCESS_HUB: Lazy<(Arc, thread::ThreadId)> = Lazy::new(|| { +static PROCESS_HUB: LazyLock<(Arc, thread::ThreadId)> = LazyLock::new(|| { ( Arc::new(Hub::new(None, Arc::new(Default::default()))), thread::current().id(), @@ -21,12 +19,17 @@ thread_local! { ); } -pub(crate) struct SwitchGuard { +/// A Hub switch guard used to temporarily swap +/// active hub in thread local storage. +pub struct SwitchGuard { inner: Option<(Arc, bool)>, } impl SwitchGuard { - pub(crate) fn new(mut hub: Arc) -> Self { + /// Swaps the current thread's Hub by the one provided + /// and returns a guard that, when dropped, replaces it + /// to the previous one. + pub fn new(mut hub: Arc) -> Self { let inner = THREAD_HUB.with(|(thread_hub, is_process_hub)| { // SAFETY: `thread_hub` will always be a valid thread local hub, // by definition not shared between threads. @@ -85,11 +88,7 @@ impl HubImpl { Ok(guard) => guard, }; - guard - .top() - .client - .as_ref() - .map_or(false, |c| c.is_enabled()) + guard.top().client.as_ref().is_some_and(|c| c.is_enabled()) } } @@ -162,7 +161,7 @@ impl Hub { /// will return the provided hub. /// /// Once the function is finished executing, including after it - /// paniced, the original hub is re-installed if one was present. + /// panicked, the original hub is re-installed if one was present. pub fn run R, R>(hub: Arc, f: F) -> R { let _guard = SwitchGuard::new(hub); f() diff --git a/sentry-core/src/intodsn.rs b/sentry-core/src/intodsn.rs index 3e5b5ebf5..276d6789d 100644 --- a/sentry-core/src/intodsn.rs +++ b/sentry-core/src/intodsn.rs @@ -27,7 +27,7 @@ impl IntoDsn for () { } } -impl<'a> IntoDsn for &'a str { +impl IntoDsn for &'_ str { fn into_dsn(self) -> Result, ParseDsnError> { if self.is_empty() { Ok(None) @@ -37,14 +37,14 @@ impl<'a> IntoDsn for &'a str { } } -impl<'a> IntoDsn for Cow<'a, str> { +impl IntoDsn for Cow<'_, str> { fn into_dsn(self) -> Result, ParseDsnError> { let x: &str = &self; x.into_dsn() } } -impl<'a> IntoDsn for &'a OsStr { +impl IntoDsn for &'_ OsStr { fn into_dsn(self) -> Result, ParseDsnError> { self.to_string_lossy().into_dsn() } @@ -62,7 +62,7 @@ impl IntoDsn for String { } } -impl<'a> IntoDsn for &'a Dsn { +impl IntoDsn for &'_ Dsn { fn into_dsn(self) -> Result, ParseDsnError> { Ok(Some(self.clone())) } diff --git a/sentry-core/src/lib.rs b/sentry-core/src/lib.rs index f29454e66..a39e63ca5 100644 --- a/sentry-core/src/lib.rs +++ b/sentry-core/src/lib.rs @@ -93,9 +93,6 @@ //! - `feature = "test"`: Activates the [`test`] module, which can be used to //! write integration tests. It comes with a test transport which can capture //! all sent events for inspection. -//! - `feature = "debug-logs"`: Uses the `log` crate for debug output, instead -//! of printing to `stderr`. This feature is **deprecated** and will be -//! replaced by a dedicated log callback in the future. //! //! [Sentry]: https://sentry.io/ //! [`sentry`]: https://crates.io/crates/sentry @@ -126,7 +123,7 @@ mod transport; // public api or exports from this crate pub use crate::api::*; pub use crate::breadcrumbs::IntoBreadcrumbs; -pub use crate::clientoptions::{ClientOptions, SessionMode}; +pub use crate::clientoptions::{BeforeCallback, ClientOptions, SessionMode}; pub use crate::error::{capture_error, event_from_error, parse_type_from_debug}; pub use crate::futures::{SentryFuture, SentryFutureExt}; pub use crate::hub::Hub; @@ -135,22 +132,24 @@ pub use crate::intodsn::IntoDsn; pub use crate::performance::*; pub use crate::scope::{Scope, ScopeGuard}; pub use crate::transport::{Transport, TransportFactory}; +#[cfg(feature = "logs")] +mod logger; // structured logging macros exported with `#[macro_export]` -#[cfg(all(feature = "client", feature = "UNSTABLE_cadence"))] -pub mod cadence; // client feature #[cfg(feature = "client")] mod client; #[cfg(feature = "client")] mod hub_impl; -#[cfg(all(feature = "client", feature = "UNSTABLE_metrics"))] -pub mod metrics; +#[cfg(all(feature = "client", feature = "logs"))] +mod logs; #[cfg(feature = "client")] mod session; -#[cfg(all(feature = "client", feature = "UNSTABLE_metrics"))] -mod units; + +#[cfg(feature = "client")] +pub use crate::clientoptions::MaxRequestBodySize; + #[cfg(feature = "client")] -pub use crate::client::Client; +pub use crate::{client::Client, hub_impl::SwitchGuard as HubSwitchGuard}; // test utilities #[cfg(feature = "test")] @@ -161,3 +160,6 @@ pub mod test; pub use sentry_types as types; pub use sentry_types::protocol::v7 as protocol; pub use sentry_types::protocol::v7::{Breadcrumb, Envelope, Level, User}; + +// utilities reused across integrations +pub mod utils; diff --git a/sentry-core/src/logger.rs b/sentry-core/src/logger.rs new file mode 100644 index 000000000..293bf3740 --- /dev/null +++ b/sentry-core/src/logger.rs @@ -0,0 +1,333 @@ +//! Macros for Sentry [structured logging](https://docs.sentry.io/product/explore/logs/). + +// Helper macro to capture a log at the given level. Should not be used directly. +#[doc(hidden)] +#[macro_export] +macro_rules! logger_log { + // Simple message + ($level:expr, $msg:literal) => {{ + let log = $crate::protocol::Log { + level: $level, + body: $msg.to_owned(), + trace_id: None, + timestamp: ::std::time::SystemTime::now(), + severity_number: None, + attributes: $crate::protocol::Map::new(), + }; + $crate::Hub::current().capture_log(log) + }}; + + // Message with format string and args + ($level:expr, $fmt:literal, $($arg:expr),+) => {{ + let mut attributes = $crate::protocol::Map::new(); + + attributes.insert( + "sentry.message.template".to_owned(), + $crate::protocol::LogAttribute($crate::protocol::Value::from($fmt)) + ); + let mut i = 0; + $( + attributes.insert( + format!("sentry.message.parameter.{}", i), + $crate::protocol::LogAttribute($crate::protocol::Value::from($arg)) + ); + i += 1; + )* + let _ = i; // avoid triggering the `unused_assignments` lint + + let log = $crate::protocol::Log { + level: $level, + body: format!($fmt, $($arg),*), + trace_id: None, + timestamp: ::std::time::SystemTime::now(), + severity_number: None, + attributes, + }; + $crate::Hub::current().capture_log(log) + }}; + + // Attributes entrypoint + ($level:expr, $($rest:tt)+) => {{ + let mut attributes = $crate::protocol::Map::new(); + $crate::logger_log!(@internal attributes, $level, $($rest)+) + }}; + + // Attributes base case: no more attributes, simple message + (@internal $attrs:ident, $level:expr, $msg:literal) => {{ + let log = $crate::protocol::Log { + level: $level, + body: $msg.to_owned(), + trace_id: None, + timestamp: ::std::time::SystemTime::now(), + severity_number: None, + #[allow(clippy::redundant_field_names)] + attributes: $attrs, + }; + $crate::Hub::current().capture_log(log) + }}; + + // Attributes base case: no more attributes, message with format string and args + (@internal $attrs:ident, $level:expr, $fmt:literal, $($arg:expr),+) => {{ + $attrs.insert( + "sentry.message.template".to_owned(), + $crate::protocol::LogAttribute($crate::protocol::Value::from($fmt)) + ); + let mut i = 0; + $( + $attrs.insert( + format!("sentry.message.parameter.{}", i), + $crate::protocol::LogAttribute($crate::protocol::Value::from($arg)) + ); + i += 1; + )* + let _ = i; // avoid triggering the `unused_assignments` lint + + let log = $crate::protocol::Log { + level: $level, + body: format!($fmt, $($arg),*), + trace_id: None, + timestamp: ::std::time::SystemTime::now(), + severity_number: None, + #[allow(clippy::redundant_field_names)] + attributes: $attrs, + }; + $crate::Hub::current().capture_log(log) + }}; + + // Attributes recursive case + (@internal $attrs:ident, $level:expr, $($key:ident).+ = $value:expr, $($rest:tt)+) => {{ + $attrs.insert( + stringify!($($key).+).to_owned(), + $crate::protocol::LogAttribute($crate::protocol::Value::from($value)) + ); + $crate::logger_log!(@internal $attrs, $level, $($rest)+) + }}; +} + +/// Captures a log at the trace level, with the given message and attributes. +/// +/// To attach attributes to a log, pass them with the `key = value` syntax before the message. +/// The message can be a simple string or a format string with its arguments. +/// +/// The supported attribute keys are all valid Rust identifiers with up to 8 dots. +/// Using dots will nest multiple attributes under their common prefix in the UI. +/// +/// The supported attribute values are simple types, such as string, numbers, and boolean. +/// +/// # Examples +/// +/// ``` +/// use sentry::logger_trace; +/// +/// // Simple message +/// logger_trace!("Hello world"); +/// +/// // Message with format args +/// logger_trace!("Value is {}", 42); +/// +/// // Message with format args and attributes +/// logger_trace!( +/// error_code = 500, +/// user.id = "12345", +/// user.email = "test@test.com", +/// success = false, +/// "Error occurred: {}", +/// "bad input" +/// ); +/// ``` +#[macro_export] +macro_rules! logger_trace { + ($($arg:tt)+) => { + $crate::logger_log!($crate::protocol::LogLevel::Trace, $($arg)+) + }; +} + +/// Captures a log at the debug level, with the given message and attributes. +/// +/// To attach attributes to a log, pass them with the `key = value` syntax before the message. +/// The message can be a simple string or a format string with its arguments. +/// +/// The supported attribute keys are all valid Rust identifiers with up to 8 dots. +/// Using dots will nest multiple attributes under their common prefix in the UI. +/// +/// The supported attribute values are simple types, such as string, numbers, and boolean. +/// +/// # Examples +/// +/// ``` +/// use sentry::logger_debug; +/// +/// // Simple message +/// logger_debug!("Hello world"); +/// +/// // Message with format args +/// logger_debug!("Value is {}", 42); +/// +/// // Message with format args and attributes +/// logger_debug!( +/// error_code = 500, +/// user.id = "12345", +/// user.email = "test@test.com", +/// success = false, +/// "Error occurred: {}", +/// "bad input" +/// ); +/// ``` +#[macro_export] +macro_rules! logger_debug { + ($($arg:tt)+) => { + $crate::logger_log!($crate::protocol::LogLevel::Debug, $($arg)+) + }; +} + +/// Captures a log at the info level, with the given message and attributes. +/// +/// To attach attributes to a log, pass them with the `key = value` syntax before the message. +/// The message can be a simple string or a format string with its arguments. +/// +/// The supported attribute keys are all valid Rust identifiers with up to 8 dots. +/// Using dots will nest multiple attributes under their common prefix in the UI. +/// +/// The supported attribute values are simple types, such as string, numbers, and boolean. +/// +/// # Examples +/// +/// ``` +/// use sentry::logger_info; +/// +/// // Simple message +/// logger_info!("Hello world"); +/// +/// // Message with format args +/// logger_info!("Value is {}", 42); +/// +/// // Message with format args and attributes +/// logger_info!( +/// error_code = 500, +/// user.id = "12345", +/// user.email = "test@test.com", +/// success = false, +/// "Error occurred: {}", +/// "bad input" +/// ); +/// ``` +#[macro_export] +macro_rules! logger_info { + ($($arg:tt)+) => { + $crate::logger_log!($crate::protocol::LogLevel::Info, $($arg)+) + }; +} + +/// Captures a log at the warn level, with the given message and attributes. +/// +/// To attach attributes to a log, pass them with the `key = value` syntax before the message. +/// The message can be a simple string or a format string with its arguments. +/// +/// The supported attribute keys are all valid Rust identifiers with up to 8 dots. +/// Using dots will nest multiple attributes under their common prefix in the UI. +/// +/// The supported attribute values are simple types, such as string, numbers, and boolean. +/// +/// # Examples +/// +/// ``` +/// use sentry::logger_warn; +/// +/// // Simple message +/// logger_warn!("Hello world"); +/// +/// // Message with format args +/// logger_warn!("Value is {}", 42); +/// +/// // Message with format args and attributes +/// logger_warn!( +/// error_code = 500, +/// user.id = "12345", +/// user.email = "test@test.com", +/// success = false, +/// "Error occurred: {}", +/// "bad input" +/// ); +/// ``` +#[macro_export] +macro_rules! logger_warn { + ($($arg:tt)+) => { + $crate::logger_log!($crate::protocol::LogLevel::Warn, $($arg)+) + }; +} + +/// Captures a log at the error level, with the given message and attributes. +/// +/// To attach attributes to a log, pass them with the `key = value` syntax before the message. +/// The message can be a simple string or a format string with its arguments. +/// +/// The supported attribute keys are all valid Rust identifiers with up to 8 dots. +/// Using dots will nest multiple attributes under their common prefix in the UI. +/// +/// The supported attribute values are simple types, such as string, numbers, and boolean. +/// +/// # Examples +/// +/// ``` +/// use sentry::logger_error; +/// +/// // Simple message +/// logger_error!("Hello world"); +/// +/// // Message with format args +/// logger_error!("Value is {}", 42); +/// +/// // Message with format args and attributes +/// logger_error!( +/// error_code = 500, +/// user.id = "12345", +/// user.email = "test@test.com", +/// success = false, +/// "Error occurred: {}", +/// "bad input" +/// ); +/// ``` +#[macro_export] +macro_rules! logger_error { + ($($arg:tt)+) => { + $crate::logger_log!($crate::protocol::LogLevel::Error, $($arg)+) + }; +} + +/// Captures a log at the fatal level, with the given message and attributes. +/// +/// To attach attributes to a log, pass them with the `key = value` syntax before the message. +/// The message can be a simple string or a format string with its arguments. +/// +/// The supported attribute keys are all valid Rust identifiers with up to 8 dots. +/// Using dots will nest multiple attributes under their common prefix in the UI. +/// +/// The supported attribute values are simple types, such as string, numbers, and boolean. +/// +/// # Examples +/// +/// ``` +/// use sentry::logger_fatal; +/// +/// // Simple message +/// logger_fatal!("Hello world"); +/// +/// // Message with format args +/// logger_fatal!("Value is {}", 42); +/// +/// // Message with format args and attributes +/// logger_fatal!( +/// error_code = 500, +/// user.id = "12345", +/// user.email = "test@test.com", +/// success = false, +/// "Error occurred: {}", +/// "bad input" +/// ); +/// ``` +#[macro_export] +macro_rules! logger_fatal { + ($($arg:tt)+) => { + $crate::logger_log!($crate::protocol::LogLevel::Fatal, $($arg)+) + }; +} diff --git a/sentry-core/src/logs.rs b/sentry-core/src/logs.rs new file mode 100644 index 000000000..9be3ee335 --- /dev/null +++ b/sentry-core/src/logs.rs @@ -0,0 +1,197 @@ +//! Batching for Sentry [structured logs](https://docs.sentry.io/product/explore/logs/). + +use std::sync::{Arc, Condvar, Mutex, MutexGuard}; +use std::thread::JoinHandle; +use std::time::{Duration, Instant}; + +use crate::client::TransportArc; +use crate::protocol::EnvelopeItem; +use crate::Envelope; +use sentry_types::protocol::v7::Log; + +// Flush when there's 100 logs in the buffer +const MAX_LOG_ITEMS: usize = 100; +// Or when 5 seconds have passed from the last flush +const FLUSH_INTERVAL: Duration = Duration::from_secs(5); + +#[derive(Debug, Default)] +struct LogQueue { + logs: Vec, +} + +/// Accumulates logs in the queue and submits them through the transport when one of the flushing +/// conditions is met. +pub(crate) struct LogsBatcher { + transport: TransportArc, + queue: Arc>, + shutdown: Arc<(Mutex, Condvar)>, + worker: Option>, +} + +impl LogsBatcher { + /// Creates a new LogsBatcher that will submit envelopes to the given `transport`. + pub(crate) fn new(transport: TransportArc) -> Self { + let queue = Arc::new(Mutex::new(Default::default())); + #[allow(clippy::mutex_atomic)] + let shutdown = Arc::new((Mutex::new(false), Condvar::new())); + + let worker_transport = transport.clone(); + let worker_queue = queue.clone(); + let worker_shutdown = shutdown.clone(); + let worker = std::thread::Builder::new() + .name("sentry-logs-batcher".into()) + .spawn(move || { + let (lock, cvar) = worker_shutdown.as_ref(); + let mut shutdown = lock.lock().unwrap(); + // check this immediately, in case the main thread is already shutting down + if *shutdown { + return; + } + let mut last_flush = Instant::now(); + loop { + let timeout = FLUSH_INTERVAL + .checked_sub(last_flush.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + shutdown = cvar.wait_timeout(shutdown, timeout).unwrap().0; + if *shutdown { + return; + } + if last_flush.elapsed() >= FLUSH_INTERVAL { + LogsBatcher::flush_queue_internal( + worker_queue.lock().unwrap(), + &worker_transport, + ); + last_flush = Instant::now(); + } + } + }) + .unwrap(); + + Self { + transport, + queue, + shutdown, + worker: Some(worker), + } + } + + /// Enqueues a log for delayed sending. + /// + /// This will automatically flush the queue if it reaches a size of `BATCH_SIZE`. + pub(crate) fn enqueue(&self, log: Log) { + let mut queue = self.queue.lock().unwrap(); + queue.logs.push(log); + if queue.logs.len() >= MAX_LOG_ITEMS { + LogsBatcher::flush_queue_internal(queue, &self.transport); + } + } + + /// Flushes the queue to the transport. + pub(crate) fn flush(&self) { + let queue = self.queue.lock().unwrap(); + LogsBatcher::flush_queue_internal(queue, &self.transport); + } + + /// Flushes the queue to the transport. + /// + /// This is a static method as it will be called from both the background + /// thread and the main thread on drop. + fn flush_queue_internal(mut queue_lock: MutexGuard, transport: &TransportArc) { + let logs = std::mem::take(&mut queue_lock.logs); + drop(queue_lock); + + if logs.is_empty() { + return; + } + + sentry_debug!("[LogsBatcher] Flushing {} logs", logs.len()); + + if let Some(ref transport) = *transport.read().unwrap() { + let mut envelope = Envelope::new(); + let logs_item: EnvelopeItem = logs.into(); + envelope.add_item(logs_item); + transport.send_envelope(envelope); + } + } +} + +impl Drop for LogsBatcher { + fn drop(&mut self) { + let (lock, cvar) = self.shutdown.as_ref(); + *lock.lock().unwrap() = true; + cvar.notify_one(); + + if let Some(worker) = self.worker.take() { + worker.join().ok(); + } + LogsBatcher::flush_queue_internal(self.queue.lock().unwrap(), &self.transport); + } +} + +#[cfg(all(test, feature = "test"))] +mod tests { + use crate::logger_info; + use crate::test; + + // Test that logs are sent in batches + #[test] + fn test_logs_batching() { + let envelopes = test::with_captured_envelopes_options( + || { + for i in 0..150 { + logger_info!("test log {}", i); + } + }, + crate::ClientOptions { + enable_logs: true, + ..Default::default() + }, + ); + + assert_eq!(2, envelopes.len()); + + let mut total_logs = 0; + for envelope in &envelopes { + for item in envelope.items() { + if let crate::protocol::EnvelopeItem::ItemContainer( + crate::protocol::ItemContainer::Logs(logs), + ) = item + { + total_logs += logs.len(); + } + } + } + + assert_eq!(150, total_logs); + } + + // Test that the batcher is flushed on client close + #[test] + fn test_logs_batcher_flush() { + let envelopes = test::with_captured_envelopes_options( + || { + for i in 0..12 { + logger_info!("test log {}", i); + } + }, + crate::ClientOptions { + enable_logs: true, + ..Default::default() + }, + ); + + assert_eq!(1, envelopes.len()); + + for envelope in &envelopes { + for item in envelope.items() { + if let crate::protocol::EnvelopeItem::ItemContainer( + crate::protocol::ItemContainer::Logs(logs), + ) = item + { + assert_eq!(12, logs.len()); + break; + } + } + } + } +} diff --git a/sentry-core/src/macros.rs b/sentry-core/src/macros.rs index 34209d05a..edb75dcd5 100644 --- a/sentry-core/src/macros.rs +++ b/sentry-core/src/macros.rs @@ -55,17 +55,12 @@ macro_rules! with_client_impl { #[doc(hidden)] macro_rules! sentry_debug { ($($arg:tt)*) => { - #[cfg(feature = "debug-logs")] { - ::log::debug!(target: "sentry", $($arg)*); - } - #[cfg(not(feature = "debug-logs"))] { - $crate::Hub::with(|hub| { - if hub.client().map_or(false, |c| c.options().debug) { - eprint!("[sentry] "); - eprintln!($($arg)*); - } - }); - } + $crate::Hub::with(|hub| { + if hub.client().map_or(false, |c| c.options().debug) { + eprint!("[sentry] "); + eprintln!($($arg)*); + } + }); } } diff --git a/sentry-core/src/metrics.rs b/sentry-core/src/metrics.rs deleted file mode 100644 index 0e8b6f3b9..000000000 --- a/sentry-core/src/metrics.rs +++ /dev/null @@ -1,1201 +0,0 @@ -//! Utilities to track metrics in Sentry. -//! -//! Metrics are numerical values that can track anything about your environment over time, from -//! latency to error rates to user signups. -//! -//! Metrics at Sentry come in different flavors, in order to help you track your data in the most -//! efficient and cost-effective way. The types of metrics we currently support are: -//! -//! - **Counters** track a value that can only be incremented. -//! - **Distributions** track a list of values over time in on which you can perform aggregations -//! like max, min, avg. -//! - **Gauges** track a value that can go up and down. -//! - **Sets** track a set of values on which you can perform aggregations such as count_unique. -//! -//! For more information on metrics in Sentry, see [our docs]. -//! -//! # Usage -//! -//! To collect a metric, use the [`Metric`] struct to capture all relevant properties of your -//! metric. Then, use [`send`](Metric::send) to send the metric to Sentry: -//! -//! ``` -//! use std::time::Duration; -//! use sentry::metrics::Metric; -//! -//! Metric::count("requests") -//! .with_tag("method", "GET") -//! .send(); -//! -//! Metric::timing("request.duration", Duration::from_millis(17)) -//! .with_tag("status_code", "200") -//! // unit is added automatically by timing -//! .send(); -//! -//! Metric::set("site.visitors", "user1") -//! .with_unit("user") -//! .send(); -//! ``` -//! -//! # Usage with Cadence -//! -//! [`cadence`] is a popular Statsd client for Rust and can be used to send metrics to Sentry. To -//! use Sentry directly with `cadence`, see the [`sentry-cadence`](crate::cadence) documentation. -//! -//! [our docs]: https://develop.sentry.dev/delightful-developer-metrics/ - -use std::borrow::Cow; -use std::collections::hash_map::{DefaultHasher, Entry}; -use std::collections::{BTreeMap, BTreeSet, HashMap}; -use std::fmt::{self, Write}; -use std::sync::{Arc, Mutex}; -use std::thread::{self, JoinHandle}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -use sentry_types::protocol::latest::{Envelope, EnvelopeItem}; - -use crate::client::TransportArc; -use crate::{ClientOptions, Hub}; - -pub use crate::units::*; - -const BUCKET_INTERVAL: Duration = Duration::from_secs(10); -const FLUSH_INTERVAL: Duration = Duration::from_secs(5); -const MAX_WEIGHT: usize = 100_000; - -/// Type alias for strings used in [`Metric`] for names and tags. -pub type MetricStr = Cow<'static, str>; - -/// Type used for [`MetricValue::Counter`]. -pub type CounterValue = f64; - -/// Type used for [`MetricValue::Distribution`]. -pub type DistributionValue = f64; - -/// Type used for [`MetricValue::Set`]. -pub type SetValue = u32; - -/// Type used for [`MetricValue::Gauge`]. -pub type GaugeValue = f64; - -/// The value of a [`Metric`], indicating its type. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum MetricValue { - /// Counts instances of an event. - /// - /// Counters can be incremented and decremented. The default operation is to increment a counter - /// by `1`, although increments by larger values and even floating point values are possible. - /// - /// # Example - /// - /// ``` - /// use sentry::metrics::{Metric, MetricValue}; - /// - /// Metric::build("my.counter", MetricValue::Counter(1.0)).send(); - /// ``` - Counter(CounterValue), - - /// Builds a statistical distribution over values reported. - /// - /// Based on individual reported values, distributions allow to query the maximum, minimum, or - /// average of the reported values, as well as statistical quantiles. With an increasing number - /// of values in the distribution, its accuracy becomes approximate. - /// - /// # Example - /// - /// ``` - /// use sentry::metrics::{Metric, MetricValue}; - /// - /// Metric::build("my.distribution", MetricValue::Distribution(42.0)).send(); - /// ``` - Distribution(DistributionValue), - - /// Counts the number of unique reported values. - /// - /// Sets allow sending arbitrary discrete values, including strings, and store the deduplicated - /// count. With an increasing number of unique values in the set, its accuracy becomes - /// approximate. It is not possible to query individual values from a set. - /// - /// # Example - /// - /// To create a set value, use [`MetricValue::set_from_str`] or - /// [`MetricValue::set_from_display`]. These functions convert the provided argument into a - /// unique hash value, which is then used as the set value. - /// - /// ``` - /// use sentry::metrics::{Metric, MetricValue}; - /// - /// Metric::build("my.set", MetricValue::set_from_str("foo")).send(); - /// ``` - Set(SetValue), - - /// Stores absolute snapshots of values. - /// - /// In addition to plain [counters](Self::Counter), gauges store a snapshot of the maximum, - /// minimum and sum of all values, as well as the last reported value. Note that the "last" - /// component of this aggregation is not commutative. Which value is preserved as last value is - /// implementation-defined. - /// - /// # Example - /// - /// ``` - /// use sentry::metrics::{Metric, MetricValue}; - /// - /// Metric::build("my.gauge", MetricValue::Gauge(42.0)).send(); - /// ``` - Gauge(GaugeValue), -} - -impl MetricValue { - /// Returns a set value representing the given string. - pub fn set_from_str(string: &str) -> Self { - Self::Set(hash_set_value(string)) - } - - /// Returns a set value representing the given argument. - pub fn set_from_display(display: impl fmt::Display) -> Self { - Self::Set(hash_set_value(&display.to_string())) - } - - /// Returns the type of the metric value. - fn ty(&self) -> MetricType { - match self { - Self::Counter(_) => MetricType::Counter, - Self::Distribution(_) => MetricType::Distribution, - Self::Gauge(_) => MetricType::Gauge, - Self::Set(_) => MetricType::Set, - } - } -} - -/// Hashes the given set value. -/// -/// Sets only guarantee 32-bit accuracy, but arbitrary strings are allowed on the protocol. Upon -/// parsing, they are hashed and only used as hashes subsequently. -fn hash_set_value(string: &str) -> u32 { - use std::hash::Hasher; - let mut hasher = DefaultHasher::default(); - hasher.write(string.as_bytes()); - hasher.finish() as u32 -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -enum MetricType { - Counter, - Distribution, - Set, - Gauge, -} - -impl MetricType { - /// Return the shortcode for this metric type. - pub fn as_str(&self) -> &'static str { - match self { - MetricType::Counter => "c", - MetricType::Distribution => "d", - MetricType::Set => "s", - MetricType::Gauge => "g", - } - } -} - -impl fmt::Display for MetricType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_str()) - } -} - -impl std::str::FromStr for MetricType { - type Err = (); - - fn from_str(s: &str) -> Result { - Ok(match s { - "c" | "m" => Self::Counter, - "h" | "d" | "ms" => Self::Distribution, - "s" => Self::Set, - "g" => Self::Gauge, - _ => return Err(()), - }) - } -} - -/// A snapshot of values. -#[derive(Clone, Copy, Debug, PartialEq)] -struct GaugeSummary { - /// The last value reported in the bucket. - /// - /// This aggregation is not commutative. - pub last: GaugeValue, - /// The minimum value reported in the bucket. - pub min: GaugeValue, - /// The maximum value reported in the bucket. - pub max: GaugeValue, - /// The sum of all values reported in the bucket. - pub sum: GaugeValue, - /// The number of times this bucket was updated with a new value. - pub count: u64, -} - -impl GaugeSummary { - /// Creates a gauge snapshot from a single value. - pub fn single(value: GaugeValue) -> Self { - Self { - last: value, - min: value, - max: value, - sum: value, - count: 1, - } - } - - /// Inserts a new value into the gauge. - pub fn insert(&mut self, value: GaugeValue) { - self.last = value; - self.min = self.min.min(value); - self.max = self.max.max(value); - self.sum += value; - self.count += 1; - } -} - -/// The aggregated value of a [`Metric`] bucket. -#[derive(Debug)] -enum BucketValue { - Counter(CounterValue), - Distribution(Vec), - Set(BTreeSet), - Gauge(GaugeSummary), -} - -impl BucketValue { - /// Inserts a new value into the bucket and returns the added weight. - pub fn insert(&mut self, value: MetricValue) -> usize { - match (self, value) { - (Self::Counter(c1), MetricValue::Counter(c2)) => { - *c1 += c2; - 0 - } - (Self::Distribution(d1), MetricValue::Distribution(d2)) => { - d1.push(d2); - 1 - } - (Self::Set(s1), MetricValue::Set(s2)) => { - if s1.insert(s2) { - 1 - } else { - 0 - } - } - (Self::Gauge(g1), MetricValue::Gauge(g2)) => { - g1.insert(g2); - 0 - } - _ => panic!("invalid metric type"), - } - } - - /// Returns the number of values stored in this bucket. - pub fn weight(&self) -> usize { - match self { - BucketValue::Counter(_) => 1, - BucketValue::Distribution(v) => v.len(), - BucketValue::Set(v) => v.len(), - BucketValue::Gauge(_) => 5, - } - } -} - -impl From for BucketValue { - fn from(value: MetricValue) -> Self { - match value { - MetricValue::Counter(v) => Self::Counter(v), - MetricValue::Distribution(v) => Self::Distribution(vec![v]), - MetricValue::Gauge(v) => Self::Gauge(GaugeSummary::single(v)), - MetricValue::Set(v) => Self::Set(BTreeSet::from([v])), - } - } -} - -/// A metric value that contains a numeric value and metadata to be sent to Sentry. -/// -/// # Units -/// -/// To make the most out of metrics in Sentry, consider assigning a unit during construction. This -/// can be achieved using the [`with_unit`](MetricBuilder::with_unit) builder method. See the -/// documentation for more examples on units. -/// -/// ``` -/// use sentry::metrics::{Metric, InformationUnit}; -/// -/// Metric::distribution("request.size", 47.2) -/// .with_unit(InformationUnit::Byte) -/// .send(); -/// ``` -/// -/// # Sending Metrics -/// -/// Metrics can be sent to Sentry directly using the [`send`](MetricBuilder::send) method on the -/// constructor. This will send the metric to the [`Client`](crate::Client) on the current [`Hub`]. -/// If there is no client on the current hub, the metric is dropped. -/// -/// ``` -/// use sentry::metrics::Metric; -/// -/// Metric::count("requests") -/// .with_tag("method", "GET") -/// .send(); -/// ``` -/// -/// # Sending to a Custom Client -/// -/// Metrics can also be sent to a custom client. This is useful if you want to send metrics to a -/// different Sentry project or with different configuration. To do so, finish building the metric -/// and then call [`add_metric`](crate::Client::add_metric) to the client: -/// -/// ``` -/// use sentry::Hub; -/// use sentry::metrics::Metric; -/// -/// let metric = Metric::count("requests") -/// .with_tag("method", "GET") -/// .finish(); -/// -/// // Obtain a client from somewhere -/// if let Some(client) = Hub::current().client() { -/// client.add_metric(metric); -/// } -/// ``` -#[derive(Debug)] -pub struct Metric { - /// The name of the metric, identifying it in Sentry. - /// - /// The name should consist of - name: MetricStr, - unit: MetricUnit, - value: MetricValue, - tags: BTreeMap, - time: Option, -} - -impl Metric { - /// Creates a new metric with the stated name and value. - /// - /// The provided name identifies the metric in Sentry. It should consist of alphanumeric - /// characters and `_`, `-`, and `.`. While a single forward slash (`/`) is also allowed in - /// metric names, it has a special meaning and should not be used in regular metric names. All - /// characters that do not match this criteria are sanitized. - /// - /// The value of the metric determines its type. See the [struct-level](self) docs and - /// constructor methods for examples on how to build metrics. - pub fn build(name: impl Into, value: MetricValue) -> MetricBuilder { - let metric = Metric { - name: name.into(), - unit: MetricUnit::None, - value, - tags: BTreeMap::new(), - time: None, - }; - - MetricBuilder { metric } - } - - /// Parses a metric from a StatsD string. - /// - /// This supports regular StatsD payloads with an extension for tags. In the below example, tags - /// are optional: - /// - /// ```plain - /// :||#:,: - /// ``` - /// - /// Units are encoded into the metric name, separated by an `@`: - /// - /// ```plain - /// @:||#:,: - /// ``` - pub fn parse_statsd(string: &str) -> Result { - parse_metric_opt(string).ok_or(ParseMetricError(())) - } - - /// Builds a metric that increments a [counter](MetricValue::Counter) by the given value. - /// - /// # Example - /// - /// ``` - /// use sentry::metrics::{Metric}; - /// - /// Metric::incr("operation.total_values", 7.0).send(); - /// ``` - pub fn incr(name: impl Into, value: f64) -> MetricBuilder { - Self::build(name, MetricValue::Counter(value)) - } - - /// Builds a metric that [counts](MetricValue::Counter) the single occurrence of an event. - /// - /// # Example - /// - /// ``` - /// use sentry::metrics::{Metric}; - /// - /// Metric::count("requests").send(); - /// ``` - pub fn count(name: impl Into) -> MetricBuilder { - Self::build(name, MetricValue::Counter(1.0)) - } - - /// Builds a metric that tracks the duration of an operation. - /// - /// This is a [distribution](MetricValue::Distribution) metric that is tracked in seconds. - /// - /// # Example - /// - /// ``` - /// use std::time::Duration; - /// use sentry::metrics::{Metric}; - /// - /// Metric::timing("operation", Duration::from_secs(1)).send(); - /// ``` - pub fn timing(name: impl Into, timing: Duration) -> MetricBuilder { - Self::build(name, MetricValue::Distribution(timing.as_secs_f64())) - .with_unit(DurationUnit::Second) - } - - /// Builds a metric that tracks the [distribution](MetricValue::Distribution) of values. - /// - /// # Example - /// - /// ``` - /// use sentry::metrics::{Metric}; - /// - /// Metric::distribution("operation.batch_size", 42.0).send(); - /// ``` - pub fn distribution(name: impl Into, value: f64) -> MetricBuilder { - Self::build(name, MetricValue::Distribution(value)) - } - - /// Builds a metric that tracks the [unique number](MetricValue::Set) of values provided. - /// - /// See [`MetricValue`] for more ways to construct sets. - /// - /// # Example - /// - /// ``` - /// use sentry::metrics::{Metric}; - /// - /// Metric::set("users", "user1").send(); - /// ``` - pub fn set(name: impl Into, string: &str) -> MetricBuilder { - Self::build(name, MetricValue::set_from_str(string)) - } - - /// Builds a metric that tracks the [snapshot](MetricValue::Gauge) of provided values. - /// - /// # Example - /// - /// ``` - /// use sentry::metrics::{Metric}; - /// - /// Metric::gauge("cache.size", 42.0).send(); - /// ``` - pub fn gauge(name: impl Into, value: f64) -> MetricBuilder { - Self::build(name, MetricValue::Gauge(value)) - } - - /// Sends the metric to the current client. - /// - /// When building a metric, you can use [`MetricBuilder::send`] to send the metric directly. If - /// there is no client on the current [`Hub`], the metric is dropped. - pub fn send(self) { - if let Some(client) = Hub::current().client() { - client.add_metric(self); - } - } -} - -/// A builder for metrics. -/// -/// Use one of the [`Metric`] constructors to create a new builder. See the struct-level docs for -/// examples of how to build metrics. -#[must_use] -#[derive(Debug)] -pub struct MetricBuilder { - metric: Metric, -} - -impl MetricBuilder { - /// Sets the unit for the metric. - /// - /// The unit augments the metric value by giving it a magnitude and semantics. Some units have - /// special support when rendering metrics or their values in Sentry, such as for timings. See - /// [`MetricUnit`] for more information on the supported units. The unit can be set to - /// [`MetricUnit::None`] to indicate that the metric has no unit, or to [`MetricUnit::Custom`] - /// to indicate a user-defined unit. - /// - /// By default, the unit is set to [`MetricUnit::None`]. - pub fn with_unit(mut self, unit: impl Into) -> Self { - self.metric.unit = unit.into(); - self - } - - /// Adds a tag to the metric. - /// - /// Tags allow you to add dimensions to metrics. They are key-value pairs that can be filtered - /// or grouped by in Sentry. - /// - /// When sent to Sentry via [`MetricBuilder::send`] or when added to a - /// [`Client`](crate::Client), the client may add default tags to the metrics, such as the - /// `release` or the `environment` from the Scope. - pub fn with_tag(mut self, name: impl Into, value: impl Into) -> Self { - self.metric.tags.insert(name.into(), value.into()); - self - } - - /// Sets the timestamp for the metric. - /// - /// By default, the timestamp is set to the current time when the metric is built or sent. - pub fn with_time(mut self, time: SystemTime) -> Self { - self.metric.time = Some(time); - self - } - - /// Builds the metric. - pub fn finish(self) -> Metric { - self.metric - } - - /// Sends the metric to the current client. - /// - /// This is a shorthand for `.finish().send()`. If there is no client on the current [`Hub`], - /// the metric is dropped. - pub fn send(self) { - self.finish().send() - } -} - -/// Error emitted from [`Metric::parse_statsd`] for invalid metric strings. -#[derive(Debug)] -pub struct ParseMetricError(()); - -impl std::error::Error for ParseMetricError {} - -impl fmt::Display for ParseMetricError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("invalid metric string") - } -} - -fn parse_metric_opt(string: &str) -> Option { - let mut components = string.split('|'); - - let (mri_str, value_str) = components.next()?.split_once(':')?; - let (name, unit) = match mri_str.split_once('@') { - Some((name, unit_str)) => (name, unit_str.parse().ok()?), - None => (mri_str, MetricUnit::None), - }; - - let ty = components.next().and_then(|s| s.parse().ok())?; - let value = match ty { - MetricType::Counter => MetricValue::Counter(value_str.parse().ok()?), - MetricType::Distribution => MetricValue::Distribution(value_str.parse().ok()?), - MetricType::Set => MetricValue::Set(value_str.parse().ok()?), - MetricType::Gauge => { - // Gauge values are serialized as `last:min:max:sum:count`. We want to be able - // to parse those strings back, so we just take the first colon-separated segment. - let value_str = value_str.split(':').next().unwrap(); - MetricValue::Gauge(value_str.parse().ok()?) - } - }; - - let mut builder = Metric::build(name.to_owned(), value).with_unit(unit); - - for component in components { - if let Some('#') = component.chars().next() { - for pair in component.get(1..)?.split(',') { - let mut key_value = pair.splitn(2, ':'); - - let key = key_value.next()?.to_owned(); - let value = key_value.next().unwrap_or_default().to_owned(); - - builder = builder.with_tag(key, value); - } - } - } - - Some(builder.finish()) -} - -/// Composite bucket key for [`BucketMap`]. -#[derive(Debug, PartialEq, Eq, Hash)] -struct BucketKey { - ty: MetricType, - name: MetricStr, - unit: MetricUnit, - tags: BTreeMap, -} - -/// UNIX timestamp used for buckets. -type Timestamp = u64; - -/// A nested map storing metric buckets. -/// -/// This map consists of two levels: -/// 1. The rounded UNIX timestamp of buckets. -/// 2. The metric buckets themselves with a corresponding timestamp. -/// -/// This structure allows for efficient dequeueing of buckets that are older than a certain -/// threshold. The buckets are dequeued in order of their timestamp, so the oldest buckets are -/// dequeued first. -type BucketMap = BTreeMap>; - -#[derive(Debug)] -struct SharedAggregatorState { - buckets: BucketMap, - weight: usize, - running: bool, - force_flush: bool, -} - -impl SharedAggregatorState { - pub fn new() -> Self { - Self { - buckets: BTreeMap::new(), - weight: 0, - running: true, - force_flush: false, - } - } - - /// Adds a new bucket to the aggregator. - /// - /// The bucket timestamp is rounded to the nearest bucket interval. Note that this does NOT - /// automatically flush the aggregator if the weight exceeds the weight threshold. - pub fn add(&mut self, mut timestamp: Timestamp, key: BucketKey, value: MetricValue) { - // Floor timestamp to bucket interval - timestamp /= BUCKET_INTERVAL.as_secs(); - timestamp *= BUCKET_INTERVAL.as_secs(); - - match self.buckets.entry(timestamp).or_default().entry(key) { - Entry::Occupied(mut e) => self.weight += e.get_mut().insert(value), - Entry::Vacant(e) => self.weight += e.insert(value.into()).weight(), - } - } - - /// Removes and returns all buckets that are ready to flush. - /// - /// Buckets are ready to flush as soon as their time window has closed. For example, a bucket - /// from timestamps `[4600, 4610)` is ready to flush immediately at `4610`. - pub fn take_buckets(&mut self) -> BucketMap { - if self.force_flush || !self.running { - self.weight = 0; - self.force_flush = false; - std::mem::take(&mut self.buckets) - } else { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .saturating_sub(BUCKET_INTERVAL) - .as_secs(); - - // Split all buckets after the cutoff time. `split` contains newer buckets, which should - // remain, so swap them. After the swap, `split` contains all older buckets. - let mut split = self.buckets.split_off(×tamp); - std::mem::swap(&mut split, &mut self.buckets); - - self.weight -= split - .values() - .flat_map(|map| map.values()) - .map(|bucket| bucket.weight()) - .sum::(); - - split - } - } - - pub fn weight(&self) -> usize { - self.weight - } -} - -type TagMap = BTreeMap; - -fn get_default_tags(options: &ClientOptions) -> TagMap { - let mut tags = TagMap::new(); - if let Some(ref release) = options.release { - tags.insert("release".into(), release.clone()); - } - if let Some(ref environment) = options.environment { - tags.insert("environment".into(), environment.clone()); - } - tags -} - -#[derive(Clone)] -struct Worker { - shared: Arc>, - default_tags: TagMap, - transport: TransportArc, -} - -impl Worker { - pub fn run(self) { - loop { - // Park instead of sleep so we can wake the thread up. Do not account for delays during - // flushing, since we benefit from some drift to spread out metric submissions. - thread::park_timeout(FLUSH_INTERVAL); - - let buckets = { - let mut guard = self.shared.lock().unwrap(); - if !guard.running { - break; - } - guard.take_buckets() - }; - - self.flush_buckets(buckets); - } - } - - pub fn flush_buckets(&self, buckets: BucketMap) { - if buckets.is_empty() { - return; - } - - // The transport is usually available when flush is called. Prefer a short lock and worst - // case throw away the result rather than blocking the transport for too long. - if let Ok(output) = self.format_payload(buckets) { - let mut envelope = Envelope::new(); - envelope.add_item(EnvelopeItem::Statsd(output)); - - if let Some(ref transport) = *self.transport.read().unwrap() { - transport.send_envelope(envelope); - } - } - } - - fn format_payload(&self, buckets: BucketMap) -> std::io::Result> { - use std::io::Write; - let mut out = vec![]; - - for (timestamp, buckets) in buckets { - for (key, value) in buckets { - write!(&mut out, "{}", SafeKey(key.name.as_ref()))?; - if key.unit != MetricUnit::None { - write!(&mut out, "@{}", key.unit)?; - } - - match value { - BucketValue::Counter(c) => { - write!(&mut out, ":{}", c)?; - } - BucketValue::Distribution(d) => { - for v in d { - write!(&mut out, ":{}", v)?; - } - } - BucketValue::Set(s) => { - for v in s { - write!(&mut out, ":{}", v)?; - } - } - BucketValue::Gauge(g) => { - write!( - &mut out, - ":{}:{}:{}:{}:{}", - g.last, g.min, g.max, g.sum, g.count - )?; - } - } - - write!(&mut out, "|{}", key.ty.as_str())?; - - for (i, (k, v)) in key.tags.iter().chain(&self.default_tags).enumerate() { - match i { - 0 => write!(&mut out, "|#")?, - _ => write!(&mut out, ",")?, - } - - write!(&mut out, "{}:{}", SafeKey(k.as_ref()), SafeVal(v.as_ref()))?; - } - - writeln!(&mut out, "|T{}", timestamp)?; - } - } - - Ok(out) - } -} - -impl fmt::Debug for Worker { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Worker") - .field("transport", &format_args!("ArcTransport")) - .field("default_tags", &self.default_tags) - .finish() - } -} - -#[derive(Debug)] -pub(crate) struct MetricAggregator { - local_worker: Worker, - handle: Option>, -} - -impl MetricAggregator { - pub fn new(transport: TransportArc, options: &ClientOptions) -> Self { - let worker = Worker { - shared: Arc::new(Mutex::new(SharedAggregatorState::new())), - default_tags: get_default_tags(options), - transport, - }; - - let local_worker = worker.clone(); - - let handle = thread::Builder::new() - .name("sentry-metrics".into()) - .spawn(move || worker.run()) - .expect("failed to spawn thread"); - - Self { - local_worker, - handle: Some(handle), - } - } - - pub fn add(&self, metric: Metric) { - let Metric { - name, - unit, - value, - tags, - time, - } = metric; - - let timestamp = time - .unwrap_or_else(SystemTime::now) - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - - let key = BucketKey { - ty: value.ty(), - name, - unit, - tags, - }; - - let mut guard = self.local_worker.shared.lock().unwrap(); - guard.add(timestamp, key, value); - - if guard.weight() > MAX_WEIGHT { - if let Some(ref handle) = self.handle { - guard.force_flush = true; - handle.thread().unpark(); - } - } - } - - pub fn flush(&self) { - let buckets = { - let mut guard = self.local_worker.shared.lock().unwrap(); - guard.force_flush = true; - guard.take_buckets() - }; - - self.local_worker.flush_buckets(buckets); - } -} - -impl Drop for MetricAggregator { - fn drop(&mut self) { - let buckets = { - let mut guard = self.local_worker.shared.lock().unwrap(); - guard.running = false; - guard.take_buckets() - }; - - self.local_worker.flush_buckets(buckets); - - if let Some(handle) = self.handle.take() { - handle.thread().unpark(); - handle.join().unwrap(); - } - } -} - -fn safe_fmt(f: &mut fmt::Formatter<'_>, string: &str, mut check: F) -> fmt::Result -where - F: FnMut(char) -> bool, -{ - let mut valid = true; - - for c in string.chars() { - if check(c) { - valid = true; - f.write_char(c)?; - } else if valid { - valid = false; - f.write_char('_')?; - } - } - - Ok(()) -} - -// Helper that serializes a string into a safe format for metric names or tag keys. -struct SafeKey<'s>(&'s str); - -impl<'s> fmt::Display for SafeKey<'s> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - safe_fmt(f, self.0, |c| { - c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | '/') - }) - } -} - -// Helper that serializes a string into a safe format for tag values. -struct SafeVal<'s>(&'s str); - -impl<'s> fmt::Display for SafeVal<'s> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - safe_fmt(f, self.0, |c| { - c.is_alphanumeric() - || matches!( - c, - '_' | ':' | '/' | '@' | '.' | '{' | '}' | '[' | ']' | '$' | '-' - ) - }) - } -} - -#[cfg(test)] -mod tests { - use crate::test::{with_captured_envelopes, with_captured_envelopes_options}; - use crate::ClientOptions; - - use super::*; - - /// Returns the current system time and rounded bucket timestamp. - fn current_time() -> (SystemTime, u64) { - let now = SystemTime::now(); - let timestamp = now.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(); - let timestamp = timestamp / 10 * 10; - - (now, timestamp) - } - - fn get_single_metrics(envelopes: &[Envelope]) -> &str { - assert_eq!(envelopes.len(), 1, "expected exactly one envelope"); - - let mut items = envelopes[0].items(); - let Some(EnvelopeItem::Statsd(payload)) = items.next() else { - panic!("expected metrics item"); - }; - - std::str::from_utf8(payload).unwrap().trim() - } - - #[test] - fn test_tags() { - let (time, ts) = current_time(); - - let envelopes = with_captured_envelopes(|| { - Metric::count("my.metric") - .with_tag("foo", "bar") - .with_tag("and", "more") - .with_time(time) - .send(); - }); - - let metrics = get_single_metrics(&envelopes); - assert_eq!(metrics, format!("my.metric:1|c|#and:more,foo:bar|T{ts}")); - } - - #[test] - fn test_unit() { - let (time, ts) = current_time(); - - let envelopes = with_captured_envelopes(|| { - Metric::count("my.metric") - .with_time(time) - .with_unit("custom") - .send(); - }); - - let metrics = get_single_metrics(&envelopes); - assert_eq!(metrics, format!("my.metric@custom:1|c|T{ts}")); - } - - #[test] - fn test_metric_sanitation() { - let (time, ts) = current_time(); - - let envelopes = with_captured_envelopes(|| { - Metric::count("my$$$metric").with_time(time).send(); - }); - - let metrics = get_single_metrics(&envelopes); - assert_eq!(metrics, format!("my_metric:1|c|T{ts}")); - } - - #[test] - fn test_tag_sanitation() { - let (time, ts) = current_time(); - - let envelopes = with_captured_envelopes(|| { - Metric::count("my.metric") - .with_tag("foo-bar$$$blub", "%$föö{}") - .with_time(time) - .send(); - }); - - let metrics = get_single_metrics(&envelopes); - assert_eq!( - metrics, - format!("my.metric:1|c|#foo-bar_blub:_$föö{{}}|T{ts}") - ); - } - - #[test] - fn test_own_namespace() { - let (time, ts) = current_time(); - - let envelopes = with_captured_envelopes(|| { - Metric::count("ns/my.metric").with_time(time).send(); - }); - - let metrics = get_single_metrics(&envelopes); - assert_eq!(metrics, format!("ns/my.metric:1|c|T{ts}")); - } - - #[test] - fn test_default_tags() { - let (time, ts) = current_time(); - - let options = ClientOptions { - release: Some("myapp@1.0.0".into()), - environment: Some("production".into()), - ..Default::default() - }; - - let envelopes = with_captured_envelopes_options( - || { - Metric::count("requests") - .with_tag("foo", "bar") - .with_time(time) - .send(); - }, - options, - ); - - let metrics = get_single_metrics(&envelopes); - assert_eq!( - metrics, - format!("requests:1|c|#foo:bar,environment:production,release:myapp@1.0.0|T{ts}") - ); - } - - #[test] - fn test_counter() { - let (time, ts) = current_time(); - - let envelopes = with_captured_envelopes(|| { - Metric::count("my.metric").with_time(time).send(); - Metric::incr("my.metric", 2.0).with_time(time).send(); - }); - - let metrics = get_single_metrics(&envelopes); - assert_eq!(metrics, format!("my.metric:3|c|T{ts}")); - } - - #[test] - fn test_timing() { - let (time, ts) = current_time(); - - let envelopes = with_captured_envelopes(|| { - Metric::timing("my.metric", Duration::from_millis(200)) - .with_time(time) - .send(); - Metric::timing("my.metric", Duration::from_millis(100)) - .with_time(time) - .send(); - }); - - let metrics = get_single_metrics(&envelopes); - assert_eq!(metrics, format!("my.metric@second:0.2:0.1|d|T{ts}")); - } - - #[test] - fn test_distribution() { - let (time, ts) = current_time(); - - let envelopes = with_captured_envelopes(|| { - Metric::distribution("my.metric", 2.0) - .with_time(time) - .send(); - Metric::distribution("my.metric", 1.0) - .with_time(time) - .send(); - }); - - let metrics = get_single_metrics(&envelopes); - assert_eq!(metrics, format!("my.metric:2:1|d|T{ts}")); - } - - #[test] - fn test_set() { - let (time, ts) = current_time(); - - let envelopes = with_captured_envelopes(|| { - Metric::set("my.metric", "hello").with_time(time).send(); - // Duplicate that should not be reflected twice - Metric::set("my.metric", "hello").with_time(time).send(); - Metric::set("my.metric", "world").with_time(time).send(); - }); - - let metrics = get_single_metrics(&envelopes); - assert_eq!(metrics, format!("my.metric:3410894750:3817476724|s|T{ts}")); - } - - #[test] - fn test_gauge() { - let (time, ts) = current_time(); - - let envelopes = with_captured_envelopes(|| { - Metric::gauge("my.metric", 2.0).with_time(time).send(); - Metric::gauge("my.metric", 1.0).with_time(time).send(); - Metric::gauge("my.metric", 1.5).with_time(time).send(); - }); - - let metrics = get_single_metrics(&envelopes); - assert_eq!(metrics, format!("my.metric:1.5:1:2:4.5:3|g|T{ts}")); - } - - #[test] - fn test_multiple() { - let (time, ts) = current_time(); - - let envelopes = with_captured_envelopes(|| { - Metric::count("my.metric").with_time(time).send(); - Metric::distribution("my.dist", 2.0).with_time(time).send(); - }); - - let metrics = get_single_metrics(&envelopes); - println!("{metrics}"); - - assert!(metrics.contains(&format!("my.metric:1|c|T{ts}"))); - assert!(metrics.contains(&format!("my.dist:2|d|T{ts}"))); - } - - #[test] - fn test_regression_parse_statsd() { - let payload = "docker.net.bytes_rcvd:27763.20237096717:27763.20237096717:27763.20237096717:27763.20237096717:1|g|#container_id:97df61f5c55b58ec9c04da3e03edc8a875ec90eb405eb5645ad9a86d0a7cd3ee,container_name:app_sidekiq_1"; - let metric = Metric::parse_statsd(payload).unwrap(); - assert_eq!(metric.name, "docker.net.bytes_rcvd"); - assert_eq!(metric.value, MetricValue::Gauge(27763.20237096717)); - assert_eq!( - metric.tags["container_id"], - "97df61f5c55b58ec9c04da3e03edc8a875ec90eb405eb5645ad9a86d0a7cd3ee" - ); - assert_eq!(metric.tags["container_name"], "app_sidekiq_1"); - } -} diff --git a/sentry-core/src/performance.rs b/sentry-core/src/performance.rs index d707c370b..9b48e911b 100644 --- a/sentry-core/src/performance.rs +++ b/sentry-core/src/performance.rs @@ -1,6 +1,10 @@ +use std::borrow::Cow; use std::collections::BTreeMap; use std::ops::{Deref, DerefMut}; use std::sync::{Arc, Mutex, MutexGuard}; +use std::time::SystemTime; + +use sentry_types::protocol::v7::SpanId; use crate::{protocol, Hub}; @@ -30,6 +34,23 @@ pub fn start_transaction(ctx: TransactionContext) -> Transaction { } } +/// Start a new Performance Monitoring Transaction with the provided start timestamp. +/// +/// The transaction needs to be explicitly finished via [`Transaction::finish`], +/// otherwise it will be discarded. +/// The transaction itself also represents the root span in the span hierarchy. +/// Child spans can be started with the [`Transaction::start_child`] method. +pub fn start_transaction_with_timestamp( + ctx: TransactionContext, + timestamp: SystemTime, +) -> Transaction { + let transaction = start_transaction(ctx); + if let Some(tx) = transaction.inner.lock().unwrap().transaction.as_mut() { + tx.start_timestamp = timestamp; + } + transaction +} + // Hub API: impl Hub { @@ -46,6 +67,21 @@ impl Hub { Transaction::new_noop(ctx) } } + + /// Start a new Performance Monitoring Transaction with the provided start timestamp. + /// + /// See the global [`start_transaction_with_timestamp`] for more documentation. + pub fn start_transaction_with_timestamp( + &self, + ctx: TransactionContext, + timestamp: SystemTime, + ) -> Transaction { + let transaction = start_transaction(ctx); + if let Some(tx) = transaction.inner.lock().unwrap().transaction.as_mut() { + tx.start_timestamp = timestamp; + } + transaction + } } // "Context" Types: @@ -68,12 +104,15 @@ pub struct TransactionContext { op: String, trace_id: protocol::TraceId, parent_span_id: Option, + span_id: protocol::SpanId, sampled: Option, custom: Option, } impl TransactionContext { - /// Creates a new Transaction Context with the given `name` and `op`. + /// Creates a new Transaction Context with the given `name` and `op`. A random + /// `trace_id` is assigned. Use [`TransactionContext::new_with_trace_id`] to + /// specify a custom trace ID. /// /// See /// for an explanation of a Transaction's `name`, and @@ -84,7 +123,49 @@ impl TransactionContext { /// can be used for distributed tracing. #[must_use = "this must be used with `start_transaction`"] pub fn new(name: &str, op: &str) -> Self { - Self::continue_from_headers(name, op, vec![]) + Self::new_with_trace_id(name, op, protocol::TraceId::default()) + } + + /// Creates a new Transaction Context with the given `name`, `op`, and `trace_id`. + /// + /// See + /// for an explanation of a Transaction's `name`, and + /// for conventions + /// around an `operation`'s value. + #[must_use = "this must be used with `start_transaction`"] + pub fn new_with_trace_id(name: &str, op: &str, trace_id: protocol::TraceId) -> Self { + Self { + name: name.into(), + op: op.into(), + trace_id, + parent_span_id: None, + span_id: Default::default(), + sampled: None, + custom: None, + } + } + + /// Creates a new Transaction Context with the given `name`, `op`, `trace_id`, and + /// possibly the given `span_id` and `parent_span_id`. + /// + /// See + /// for an explanation of a Transaction's `name`, and + /// for conventions + /// around an `operation`'s value. + #[must_use = "this must be used with `start_transaction`"] + pub fn new_with_details( + name: &str, + op: &str, + trace_id: protocol::TraceId, + span_id: Option, + parent_span_id: Option, + ) -> Self { + let mut slf = Self::new_with_trace_id(name, op, trace_id); + if let Some(span_id) = span_id { + slf.span_id = span_id; + } + slf.parent_span_id = parent_span_id; + slf } /// Creates a new Transaction Context based on the distributed tracing `headers`. @@ -97,24 +178,34 @@ impl TransactionContext { op: &str, headers: I, ) -> Self { - let mut trace = None; - for (k, v) in headers.into_iter() { - if k.eq_ignore_ascii_case("sentry-trace") { - trace = parse_sentry_trace(v); - } - } - - let (trace_id, parent_span_id, sampled) = match trace { - Some(trace) => (trace.0, Some(trace.1), trace.2), - None => (protocol::TraceId::default(), None, None), - }; + parse_headers(headers) + .map(|sentry_trace| Self::continue_from_sentry_trace(name, op, &sentry_trace, None)) + .unwrap_or_else(|| Self { + name: name.into(), + op: op.into(), + trace_id: Default::default(), + parent_span_id: None, + span_id: Default::default(), + sampled: None, + custom: None, + }) + } + /// Creates a new Transaction Context based on the provided distributed tracing data, + /// optionally creating the `TransactionContext` with the provided `span_id`. + pub fn continue_from_sentry_trace( + name: &str, + op: &str, + sentry_trace: &SentryTrace, + span_id: Option, + ) -> Self { Self { name: name.into(), op: op.into(), - trace_id, - parent_span_id, - sampled, + trace_id: sentry_trace.trace_id, + parent_span_id: Some(sentry_trace.span_id), + sampled: sentry_trace.sampled, + span_id: span_id.unwrap_or_default(), custom: None, } } @@ -151,6 +242,7 @@ impl TransactionContext { op: op.into(), trace_id, parent_span_id: Some(parent_span_id), + span_id: protocol::SpanId::default(), sampled, custom: None, } @@ -184,6 +276,11 @@ impl TransactionContext { self.trace_id } + /// Get the Span ID of this Transaction. + pub fn span_id(&self) -> protocol::SpanId { + self.span_id + } + /// Get the custom context of this Transaction. pub fn custom(&self) -> Option<&CustomTransactionContext> { self.custom.as_ref() @@ -200,7 +297,8 @@ impl TransactionContext { /// /// If the context did not have this key present, None is returned. /// - /// If the context did have this key present, the value is updated, and the old value is returned. + /// If the context did have this key present, the value is updated, and the old value is + /// returned. pub fn custom_insert( &mut self, key: String, @@ -215,9 +313,83 @@ impl TransactionContext { // And set our key let existing_value = custom.insert(key, value); - std::mem::swap(&mut self.custom, &mut Some(custom)); + self.custom = Some(custom); existing_value } + + /// Creates a transaction context builder initialized with the given `name` and `op`. + /// + /// See + /// for an explanation of a Transaction's `name`, and + /// for conventions + /// around an `operation`'s value. + #[must_use] + pub fn builder(name: &str, op: &str) -> TransactionContextBuilder { + TransactionContextBuilder { + ctx: TransactionContext::new(name, op), + } + } +} + +/// A transaction context builder created by [`TransactionContext::builder`]. +pub struct TransactionContextBuilder { + ctx: TransactionContext, +} + +impl TransactionContextBuilder { + /// Defines the name of the transaction. + #[must_use] + pub fn with_name(mut self, name: String) -> Self { + self.ctx.name = name; + self + } + + /// Defines the operation of the transaction. + #[must_use] + pub fn with_op(mut self, op: String) -> Self { + self.ctx.op = op; + self + } + + /// Defines the trace ID. + #[must_use] + pub fn with_trace_id(mut self, trace_id: protocol::TraceId) -> Self { + self.ctx.trace_id = trace_id; + self + } + + /// Defines a parent span ID for the created transaction. + #[must_use] + pub fn with_parent_span_id(mut self, parent_span_id: Option) -> Self { + self.ctx.parent_span_id = parent_span_id; + self + } + + /// Defines the span ID to be used when creating the transaction. + #[must_use] + pub fn with_span_id(mut self, span_id: protocol::SpanId) -> Self { + self.ctx.span_id = span_id; + self + } + + /// Defines whether the transaction will be sampled. + #[must_use] + pub fn with_sampled(mut self, sampled: Option) -> Self { + self.ctx.sampled = sampled; + self + } + + /// Adds a custom key and value to the transaction context. + #[must_use] + pub fn with_custom(mut self, key: String, value: serde_json::Value) -> Self { + self.ctx.custom_insert(key, value); + self + } + + /// Finishes building a transaction. + pub fn finish(self) -> TransactionContext { + self.ctx + } } /// A function to be run for each new transaction, to determine the rate at which @@ -259,6 +431,14 @@ impl TransactionOrSpan { } } + /// Sets a tag to a specific value. + pub fn set_tag(&self, key: &str, value: V) { + match self { + TransactionOrSpan::Transaction(transaction) => transaction.set_tag(key, value), + TransactionOrSpan::Span(span) => span.set_tag(key, value), + } + } + /// Get the TransactionContext of the Transaction/Span. /// /// Note that this clones the underlying value. @@ -285,6 +465,22 @@ impl TransactionOrSpan { } } + /// Set the operation for this Transaction/Span. + pub fn set_op(&self, op: &str) { + match self { + TransactionOrSpan::Transaction(transaction) => transaction.set_op(op), + TransactionOrSpan::Span(span) => span.set_op(op), + } + } + + /// Set the name (description) for this Transaction/Span. + pub fn set_name(&self, name: &str) { + match self { + TransactionOrSpan::Transaction(transaction) => transaction.set_name(name), + TransactionOrSpan::Span(span) => span.set_name(name), + } + } + /// Set the HTTP request information for this Transaction/Span. pub fn set_request(&self, request: protocol::Request) { match self { @@ -294,6 +490,8 @@ impl TransactionOrSpan { } /// Returns the headers needed for distributed tracing. + /// Use [`crate::Scope::iter_trace_propagation_headers`] to obtain the active + /// trace's distributed tracing headers. pub fn iter_headers(&self) -> TraceHeadersIter { match self { TransactionOrSpan::Transaction(transaction) => transaction.iter_headers(), @@ -301,6 +499,14 @@ impl TransactionOrSpan { } } + /// Get the sampling decision for this Transaction/Span. + pub fn is_sampled(&self) -> bool { + match self { + TransactionOrSpan::Transaction(transaction) => transaction.is_sampled(), + TransactionOrSpan::Span(span) => span.is_sampled(), + } + } + /// Starts a new child Span with the given `op` and `description`. /// /// The span must be explicitly finished via [`Span::finish`], as it will @@ -313,6 +519,28 @@ impl TransactionOrSpan { } } + /// Starts a new child Span with the given `op`, `description` and `id`. + /// + /// The span must be explicitly finished via [`Span::finish`], as it will + /// otherwise not be sent to Sentry. + #[must_use = "a span must be explicitly closed via `finish()`"] + pub fn start_child_with_details( + &self, + op: &str, + description: &str, + id: SpanId, + timestamp: SystemTime, + ) -> Span { + match self { + TransactionOrSpan::Transaction(transaction) => { + transaction.start_child_with_details(op, description, id, timestamp) + } + TransactionOrSpan::Span(span) => { + span.start_child_with_details(op, description, id, timestamp) + } + } + } + #[cfg(feature = "client")] pub(crate) fn apply_to_event(&self, event: &mut protocol::Event<'_>) { if event.contexts.contains_key("trace") { @@ -335,10 +563,23 @@ impl TransactionOrSpan { event.contexts.insert("trace".into(), context.into()); } - /// Finishes the Transaction/Span. + /// Finishes the Transaction/Span with the provided end timestamp. /// /// This records the end timestamp and either sends the inner [`Transaction`] /// directly to Sentry, or adds the [`Span`] to its transaction. + pub fn finish_with_timestamp(self, timestamp: SystemTime) { + match self { + TransactionOrSpan::Transaction(transaction) => { + transaction.finish_with_timestamp(timestamp) + } + TransactionOrSpan::Span(span) => span.finish_with_timestamp(timestamp), + } + } + + /// Finishes the Transaction/Span. + /// + /// This records the current timestamp as the end timestamp and either sends the inner [`Transaction`] + /// directly to Sentry, or adds the [`Span`] to its transaction. pub fn finish(self) { match self { TransactionOrSpan::Transaction(transaction) => transaction.finish(), @@ -358,7 +599,7 @@ pub(crate) struct TransactionInner { type TransactionArc = Arc>; -/// Functional implementation of how a new transation's sample rate is chosen. +/// Functional implementation of how a new transaction's sample rate is chosen. /// /// Split out from `Client.is_transaction_sampled` for testing. #[cfg(feature = "client")] @@ -369,26 +610,33 @@ fn transaction_sample_rate( ) -> f32 { match (traces_sampler, traces_sample_rate) { (Some(traces_sampler), _) => traces_sampler(ctx), - (None, traces_sample_rate) => ctx - .sampled - .map(|sampled| if sampled { 1.0 } else { 0.0 }) - .unwrap_or(traces_sample_rate), + (None, traces_sample_rate) => ctx.sampled.map(f32::from).unwrap_or(traces_sample_rate), } } /// Determine whether the new transaction should be sampled. #[cfg(feature = "client")] impl Client { - fn is_transaction_sampled(&self, ctx: &TransactionContext) -> bool { + fn determine_sampling_decision(&self, ctx: &TransactionContext) -> (bool, f32) { let client_options = self.options(); - self.sample_should_send(transaction_sample_rate( + let sample_rate = transaction_sample_rate( client_options.traces_sampler.as_deref(), ctx, client_options.traces_sample_rate, - )) + ); + let sampled = self.sample_should_send(sample_rate); + (sampled, sample_rate) } } +/// Some metadata associated with a transaction. +#[cfg(feature = "client")] +#[derive(Clone, Debug)] +struct TransactionMetadata { + /// The sample rate used when making the sampling decision for the associated transaction. + sample_rate: f32, +} + /// A running Performance Monitoring Transaction. /// /// The transaction needs to be explicitly finished via [`Transaction::finish`], @@ -397,55 +645,71 @@ impl Client { #[derive(Clone, Debug)] pub struct Transaction { pub(crate) inner: TransactionArc, + #[cfg(feature = "client")] + metadata: TransactionMetadata, } -/// Iterable for a transaction's [`extra` field](protocol::Transaction::extra). +/// Iterable for a transaction's [data attributes](protocol::TraceContext::data). pub struct TransactionData<'a>(MutexGuard<'a, TransactionInner>); impl<'a> TransactionData<'a> { - /// Iterate over the `extra` map - /// of the [transaction][protocol::Transaction]. + /// Iterate over the [data attributes](protocol::TraceContext::data) + /// associated with this [transaction][protocol::Transaction]. /// - /// If the transaction not sampled for sending, - /// the metadata will not be populated at all + /// If the transaction is not sampled for sending, + /// the metadata will not be populated at all, /// so the produced iterator is empty. pub fn iter(&self) -> Box + '_> { - if let Some(ref rx) = self.0.transaction { - Box::new(rx.extra.iter()) + if self.0.transaction.is_some() { + Box::new(self.0.context.data.iter()) } else { Box::new(std::iter::empty()) } } + + /// Set a data attribute to be sent with this Transaction. + pub fn set_data(&mut self, key: Cow<'a, str>, value: protocol::Value) { + if self.0.transaction.is_some() { + self.0.context.data.insert(key.into(), value); + } + } + + /// Set a tag to be sent with this Transaction. + pub fn set_tag(&mut self, key: Cow<'_, str>, value: String) { + if let Some(transaction) = self.0.transaction.as_mut() { + transaction.tags.insert(key.into(), value); + } + } } impl Transaction { #[cfg(feature = "client")] - fn new(mut client: Option>, ctx: TransactionContext) -> Self { - let (sampled, mut transaction) = match client.as_ref() { + fn new(client: Option>, ctx: TransactionContext) -> Self { + let ((sampled, sample_rate), transaction) = match client.as_ref() { Some(client) => ( - client.is_transaction_sampled(&ctx), + client.determine_sampling_decision(&ctx), Some(protocol::Transaction { name: Some(ctx.name), ..Default::default() }), ), - None => (ctx.sampled.unwrap_or(false), None), + None => ( + ( + ctx.sampled.unwrap_or(false), + ctx.sampled.map_or(0.0, f32::from), + ), + None, + ), }; let context = protocol::TraceContext { trace_id: ctx.trace_id, parent_span_id: ctx.parent_span_id, + span_id: ctx.span_id, op: Some(ctx.op), ..Default::default() }; - // throw away the transaction here, which means there is nothing to send - // on `finish`. - if !sampled { - transaction = None; - client = None; - } - Self { inner: Arc::new(Mutex::new(TransactionInner { client, @@ -453,6 +717,7 @@ impl Transaction { context, transaction, })), + metadata: TransactionMetadata { sample_rate }, } } @@ -475,23 +740,40 @@ impl Transaction { } } - /// Set some extra information to be sent with this Transaction. + /// Set a data attribute to be sent with this Transaction. pub fn set_data(&self, key: &str, value: protocol::Value) { + let mut inner = self.inner.lock().unwrap(); + if inner.transaction.is_some() { + inner.context.data.insert(key.into(), value); + } + } + + /// Set some extra information to be sent with this Transaction. + pub fn set_extra(&self, key: &str, value: protocol::Value) { let mut inner = self.inner.lock().unwrap(); if let Some(transaction) = inner.transaction.as_mut() { transaction.extra.insert(key.into(), value); } } - /// Returns an iterating accessor to the transaction's [`extra` field](protocol::Transaction::extra). + /// Sets a tag to a specific value. + pub fn set_tag(&self, key: &str, value: V) { + let mut inner = self.inner.lock().unwrap(); + if let Some(transaction) = inner.transaction.as_mut() { + transaction.tags.insert(key.into(), value.to_string()); + } + } + + /// Returns an iterating accessor to the transaction's + /// [data attributes](protocol::TraceContext::data). /// /// # Concurrency - /// In order to obtain any kind of reference to the `extra` field, + /// In order to obtain any kind of reference to the `TraceContext::data` field, /// a `Mutex` needs to be locked. The returned `TransactionData` holds on to this lock /// for as long as it lives. Therefore you must take care not to keep the returned /// `TransactionData` around too long or it will never relinquish the lock and you may run into /// a deadlock. - pub fn data(&self) -> TransactionData { + pub fn data(&self) -> TransactionData<'_> { TransactionData(self.inner.lock().unwrap()) } @@ -515,6 +797,20 @@ impl Transaction { inner.context.status = Some(status); } + /// Set the operation of the Transaction. + pub fn set_op(&self, op: &str) { + let mut inner = self.inner.lock().unwrap(); + inner.context.op = Some(op.to_string()); + } + + /// Set the name of the Transaction. + pub fn set_name(&self, name: &str) { + let mut inner = self.inner.lock().unwrap(); + if let Some(transaction) = inner.transaction.as_mut() { + transaction.name = Some(name.to_string()); + } + } + /// Set the HTTP request information for this Transaction. pub fn set_request(&self, request: protocol::Request) { let mut inner = self.inner.lock().unwrap(); @@ -523,10 +819,18 @@ impl Transaction { } } + /// Sets the origin for this transaction, indicating what created it. + pub fn set_origin(&self, origin: &str) { + let mut inner = self.inner.lock().unwrap(); + inner.context.origin = Some(origin.to_owned()); + } + /// Returns the headers needed for distributed tracing. + /// Use [`crate::Scope::iter_trace_propagation_headers`] to obtain the active + /// trace's distributed tracing headers. pub fn iter_headers(&self) -> TraceHeadersIter { let inner = self.inner.lock().unwrap(); - let trace = SentryTrace( + let trace = SentryTrace::new( inner.context.trace_id, inner.context.span_id, Some(inner.sampled), @@ -536,29 +840,51 @@ impl Transaction { } } - /// Finishes the Transaction. + /// Get the sampling decision for this Transaction. + pub fn is_sampled(&self) -> bool { + self.inner.lock().unwrap().sampled + } + + /// Finishes the Transaction with the provided end timestamp. /// /// This records the end timestamp and sends the transaction together with /// all finished child spans to Sentry. - pub fn finish(self) { + pub fn finish_with_timestamp(self, _timestamp: SystemTime) { with_client_impl! {{ let mut inner = self.inner.lock().unwrap(); + + // Discard `Transaction` unless sampled. + if !inner.sampled { + return; + } + if let Some(mut transaction) = inner.transaction.take() { if let Some(client) = inner.client.take() { - transaction.finish(); + transaction.finish_with_timestamp(_timestamp); transaction .contexts .insert("trace".into(), inner.context.clone().into()); Hub::current().with_current_scope(|scope| scope.apply_to_transaction(&mut transaction)); let opts = client.options(); - transaction.release = opts.release.clone(); - transaction.environment = opts.environment.clone(); + transaction.release.clone_from(&opts.release); + transaction.environment.clone_from(&opts.environment); transaction.sdk = Some(std::borrow::Cow::Owned(client.sdk_info.clone())); + transaction.server_name.clone_from(&opts.server_name); + + let mut dsc = protocol::DynamicSamplingContext::new() + .with_trace_id(inner.context.trace_id) + .with_sample_rate(self.metadata.sample_rate) + .with_sampled(inner.sampled); + if let Some(public_key) = client.dsn().map(|dsn| dsn.public_key()) { + dsc = dsc.with_public_key(public_key.to_owned()); + } drop(inner); - let mut envelope = protocol::Envelope::new(); + let mut envelope = protocol::Envelope::new().with_headers( + protocol::EnvelopeHeaders::new().with_trace(dsc) + ); envelope.add_item(transaction); client.send_envelope(envelope) @@ -567,6 +893,14 @@ impl Transaction { }} } + /// Finishes the Transaction. + /// + /// This records the current timestamp as the end timestamp and sends the transaction together with + /// all finished child spans to Sentry. + pub fn finish(self) { + self.finish_with_timestamp(SystemTime::now()); + } + /// Starts a new child Span with the given `op` and `description`. /// /// The span must be explicitly finished via [`Span::finish`]. @@ -590,12 +924,56 @@ impl Transaction { span: Arc::new(Mutex::new(span)), } } + + /// Starts a new child Span with the given `op` and `description`. + /// + /// The span must be explicitly finished via [`Span::finish`]. + #[must_use = "a span must be explicitly closed via `finish()`"] + pub fn start_child_with_details( + &self, + op: &str, + description: &str, + id: SpanId, + timestamp: SystemTime, + ) -> Span { + let inner = self.inner.lock().unwrap(); + let span = protocol::Span { + trace_id: inner.context.trace_id, + parent_span_id: Some(inner.context.span_id), + op: Some(op.into()), + description: if description.is_empty() { + None + } else { + Some(description.into()) + }, + span_id: id, + start_timestamp: timestamp, + ..Default::default() + }; + Span { + transaction: Arc::clone(&self.inner), + sampled: inner.sampled, + span: Arc::new(Mutex::new(span)), + } + } } /// A smart pointer to a span's [`data` field](protocol::Span::data). pub struct Data<'a>(MutexGuard<'a, protocol::Span>); -impl<'a> Deref for Data<'a> { +impl Data<'_> { + /// Set some extra information to be sent with this Span. + pub fn set_data(&mut self, key: String, value: protocol::Value) { + self.0.data.insert(key, value); + } + + /// Set some tag to be sent with this Span. + pub fn set_tag(&mut self, key: String, value: String) { + self.0.tags.insert(key, value); + } +} + +impl Deref for Data<'_> { type Target = BTreeMap; fn deref(&self) -> &Self::Target { @@ -603,7 +981,7 @@ impl<'a> Deref for Data<'a> { } } -impl<'a> DerefMut for Data<'a> { +impl DerefMut for Data<'_> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0.data } @@ -629,6 +1007,12 @@ impl Span { span.data.insert(key.into(), value); } + /// Sets a tag to a specific value. + pub fn set_tag(&self, key: &str, value: V) { + let mut span = self.span.lock().unwrap(); + span.tags.insert(key.into(), value.to_string()); + } + /// Returns a smart pointer to the span's [`data` field](protocol::Span::data). /// /// Since [`Data`] implements `Deref` and `DerefMut`, this can be used to read and mutate @@ -640,7 +1024,7 @@ impl Span { /// for as long as it lives. Therefore you must take care not to keep the returned /// `Data` around too long or it will never relinquish the lock and you may run into /// a deadlock. - pub fn data(&self) -> Data { + pub fn data(&self) -> Data<'_> { Data(self.span.lock().unwrap()) } @@ -652,6 +1036,12 @@ impl Span { transaction.context.clone() } + /// Get the current span ID. + pub fn get_span_id(&self) -> protocol::SpanId { + let span = self.span.lock().unwrap(); + span.span_id + } + /// Get the status of the Span. pub fn get_status(&self) -> Option { let span = self.span.lock().unwrap(); @@ -664,6 +1054,18 @@ impl Span { span.status = Some(status); } + /// Set the operation of the Span. + pub fn set_op(&self, op: &str) { + let mut span = self.span.lock().unwrap(); + span.op = Some(op.to_string()); + } + + /// Set the name (description) of the Span. + pub fn set_name(&self, name: &str) { + let mut span = self.span.lock().unwrap(); + span.description = Some(name.to_string()); + } + /// Set the HTTP request information for this Span. pub fn set_request(&self, request: protocol::Request) { let mut span = self.span.lock().unwrap(); @@ -700,26 +1102,33 @@ impl Span { } /// Returns the headers needed for distributed tracing. + /// Use [`crate::Scope::iter_trace_propagation_headers`] to obtain the active + /// trace's distributed tracing headers. pub fn iter_headers(&self) -> TraceHeadersIter { let span = self.span.lock().unwrap(); - let trace = SentryTrace(span.trace_id, span.span_id, Some(self.sampled)); + let trace = SentryTrace::new(span.trace_id, span.span_id, Some(self.sampled)); TraceHeadersIter { sentry_trace: Some(trace.to_string()), } } - /// Finishes the Span. + /// Get the sampling decision for this Span. + pub fn is_sampled(&self) -> bool { + self.sampled + } + + /// Finishes the Span with the provided end timestamp. /// /// This will record the end timestamp and add the span to the transaction /// in which it was started. - pub fn finish(self) { + pub fn finish_with_timestamp(self, _timestamp: SystemTime) { with_client_impl! {{ let mut span = self.span.lock().unwrap(); if span.timestamp.is_some() { // the span was already finished return; } - span.finish(); + span.finish_with_timestamp(_timestamp); let mut inner = self.transaction.lock().unwrap(); if let Some(transaction) = inner.transaction.as_mut() { if transaction.spans.len() <= MAX_SPANS { @@ -729,6 +1138,14 @@ impl Span { }} } + /// Finishes the Span. + /// + /// This will record the current timestamp as the end timestamp and add the span to the + /// transaction in which it was started. + pub fn finish(self) { + self.finish_with_timestamp(SystemTime::now()); + } + /// Starts a new child Span with the given `op` and `description`. /// /// The span must be explicitly finished via [`Span::finish`]. @@ -752,8 +1169,43 @@ impl Span { span: Arc::new(Mutex::new(span)), } } + + /// Starts a new child Span with the given `op` and `description`. + /// + /// The span must be explicitly finished via [`Span::finish`]. + #[must_use = "a span must be explicitly closed via `finish()`"] + fn start_child_with_details( + &self, + op: &str, + description: &str, + id: SpanId, + timestamp: SystemTime, + ) -> Span { + let span = self.span.lock().unwrap(); + let span = protocol::Span { + trace_id: span.trace_id, + parent_span_id: Some(span.span_id), + op: Some(op.into()), + description: if description.is_empty() { + None + } else { + Some(description.into()) + }, + span_id: id, + start_timestamp: timestamp, + ..Default::default() + }; + Span { + transaction: self.transaction.clone(), + sampled: self.sampled, + span: Arc::new(Mutex::new(span)), + } + } } +/// Represents a key-value pair such as an HTTP header. +pub type TraceHeader = (&'static str, String); + /// An Iterator over HTTP header names and values needed for distributed tracing. /// /// This currently only yields the `sentry-trace` header, but other headers @@ -762,6 +1214,15 @@ pub struct TraceHeadersIter { sentry_trace: Option, } +impl TraceHeadersIter { + #[cfg(feature = "client")] + pub(crate) fn new(sentry_trace: String) -> Self { + Self { + sentry_trace: Some(sentry_trace), + } + } +} + impl Iterator for TraceHeadersIter { type Item = (&'static str, String); @@ -770,8 +1231,29 @@ impl Iterator for TraceHeadersIter { } } -#[derive(Debug, PartialEq)] -struct SentryTrace(protocol::TraceId, protocol::SpanId, Option); +/// A container for distributed tracing metadata that can be extracted from e.g. the `sentry-trace` +/// HTTP header. +#[derive(Debug, PartialEq, Clone, Copy, Default)] +pub struct SentryTrace { + pub(crate) trace_id: protocol::TraceId, + pub(crate) span_id: protocol::SpanId, + pub(crate) sampled: Option, +} + +impl SentryTrace { + /// Creates a new [`SentryTrace`] from the provided parameters + pub fn new( + trace_id: protocol::TraceId, + span_id: protocol::SpanId, + sampled: Option, + ) -> Self { + SentryTrace { + trace_id, + span_id, + sampled, + } + } +} fn parse_sentry_trace(header: &str) -> Option { let header = header.trim(); @@ -785,13 +1267,28 @@ fn parse_sentry_trace(header: &str) -> Option { _ => None, }); - Some(SentryTrace(trace_id, parent_span_id, parent_sampled)) + Some(SentryTrace::new(trace_id, parent_span_id, parent_sampled)) +} + +/// Extracts distributed tracing metadata from headers (or, generally, key-value pairs), +/// considering the values for `sentry-trace`. +pub fn parse_headers<'a, I: IntoIterator>( + headers: I, +) -> Option { + let mut trace = None; + for (k, v) in headers.into_iter() { + if k.eq_ignore_ascii_case("sentry-trace") { + trace = parse_sentry_trace(v); + break; + } + } + trace } impl std::fmt::Display for SentryTrace { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}-{}", self.0, self.1)?; - if let Some(sampled) = self.2 { + write!(f, "{}-{}", self.trace_id, self.span_id)?; + if let Some(sampled) = self.sampled { write!(f, "-{}", if sampled { '1' } else { '0' })?; } Ok(()) @@ -812,10 +1309,10 @@ mod tests { let trace = parse_sentry_trace("09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-0"); assert_eq!( trace, - Some(SentryTrace(trace_id, parent_trace_id, Some(false))) + Some(SentryTrace::new(trace_id, parent_trace_id, Some(false))) ); - let trace = SentryTrace(Default::default(), Default::default(), None); + let trace = SentryTrace::new(Default::default(), Default::default(), None); let parsed = parse_sentry_trace(&trace.to_string()); assert_eq!(parsed, Some(trace)); } @@ -834,8 +1331,11 @@ mod tests { let header = span.iter_headers().next().unwrap().1; let parsed = parse_sentry_trace(&header).unwrap(); - assert_eq!(&parsed.0.to_string(), "09e04486820349518ac7b5d2adbf6ba5"); - assert_eq!(parsed.2, Some(true)); + assert_eq!( + &parsed.trace_id.to_string(), + "09e04486820349518ac7b5d2adbf6ba5" + ); + assert_eq!(parsed.sampled, Some(true)); } #[test] diff --git a/sentry-core/src/scope/noop.rs b/sentry-core/src/scope/noop.rs index f4d47a37e..fc62120b9 100644 --- a/sentry-core/src/scope/noop.rs +++ b/sentry-core/src/scope/noop.rs @@ -1,5 +1,7 @@ use std::fmt; +#[cfg(feature = "logs")] +use crate::protocol::Log; use crate::protocol::{Context, Event, Level, User, Value}; use crate::TransactionOrSpan; @@ -110,6 +112,13 @@ impl Scope { minimal_unreachable!(); } + /// Applies the contained scoped data to fill a log. + #[cfg(feature = "logs")] + pub fn apply_to_log(&self, log: &mut Log) { + let _log = log; + minimal_unreachable!(); + } + /// Set the given [`TransactionOrSpan`] as the active span for this scope. pub fn set_span(&mut self, span: Option) { let _ = span; diff --git a/sentry-core/src/scope/real.rs b/sentry-core/src/scope/real.rs index f38ed52da..6bb2662f9 100644 --- a/sentry-core/src/scope/real.rs +++ b/sentry-core/src/scope/real.rs @@ -1,12 +1,19 @@ use std::borrow::Cow; use std::collections::{HashMap, VecDeque}; use std::fmt; -use std::sync::{Arc, Mutex, PoisonError, RwLock}; +#[cfg(feature = "release-health")] +use std::sync::Mutex; +use std::sync::{Arc, PoisonError, RwLock}; use crate::performance::TransactionOrSpan; -use crate::protocol::{Attachment, Breadcrumb, Context, Event, Level, Transaction, User, Value}; +use crate::protocol::{ + Attachment, Breadcrumb, Context, Event, Level, TraceContext, Transaction, User, Value, +}; +#[cfg(feature = "logs")] +use crate::protocol::{Log, LogAttribute}; +#[cfg(feature = "release-health")] use crate::session::Session; -use crate::Client; +use crate::{Client, SentryTrace, TraceHeader, TraceHeadersIter}; #[derive(Debug)] pub struct Stack { @@ -45,14 +52,17 @@ pub struct Scope { pub(crate) tags: Arc>, pub(crate) contexts: Arc>, pub(crate) event_processors: Arc>, + #[cfg(feature = "release-health")] pub(crate) session: Arc>>, pub(crate) span: Arc>, pub(crate) attachments: Arc>, + pub(crate) propagation_context: SentryTrace, } impl fmt::Debug for Scope { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Scope") + let mut debug_struct = f.debug_struct("Scope"); + debug_struct .field("level", &self.level) .field("fingerprint", &self.fingerprint) .field("transaction", &self.transaction) @@ -61,10 +71,15 @@ impl fmt::Debug for Scope { .field("extra", &self.extra) .field("tags", &self.tags) .field("contexts", &self.contexts) - .field("event_processors", &self.event_processors.len()) - .field("session", &self.session) + .field("event_processors", &self.event_processors.len()); + + #[cfg(feature = "release-health")] + debug_struct.field("session", &self.session); + + debug_struct .field("span", &self.span) .field("attachments", &self.attachments.len()) + .field("propagation_context", &self.propagation_context) .finish() } } @@ -134,7 +149,7 @@ impl Drop for ScopeGuard { stack.pop(); popped_depth }; - // NOTE: We need to drop the `stack` lock before panicing, as the + // NOTE: We need to drop the `stack` lock before panicking, as the // `PanicIntegration` will want to lock the `stack` itself // (through `capture_event` -> `HubImpl::with`), and would thus // result in a deadlock. @@ -197,6 +212,11 @@ impl Scope { self.user = user.map(Arc::new); } + /// Retrieves the user of the current scope. + pub fn user(&self) -> Option<&User> { + self.user.as_deref() + } + /// Sets a tag to a specific value. pub fn set_tag(&mut self, key: &str, value: V) { Arc::make_mut(&mut self.tags).insert(key.to_string(), value.to_string()); @@ -275,6 +295,8 @@ impl Scope { if let Some(span) = self.span.as_ref() { span.apply_to_event(&mut event); + } else { + self.apply_propagation_context(&mut event); } if event.transaction.is_none() { @@ -326,6 +348,57 @@ impl Scope { ); } + /// Applies the contained scoped data to a log, setting the `trace_id` and certain default + /// attributes. + #[cfg(feature = "logs")] + pub fn apply_to_log(&self, log: &mut Log) { + if let Some(span) = self.span.as_ref() { + log.trace_id = Some(span.get_trace_context().trace_id); + } else { + log.trace_id = Some(self.propagation_context.trace_id); + } + + if !log.attributes.contains_key("sentry.trace.parent_span_id") { + if let Some(span) = self.get_span() { + let span_id = match span { + crate::TransactionOrSpan::Transaction(transaction) => { + transaction.get_trace_context().span_id + } + crate::TransactionOrSpan::Span(span) => span.get_span_id(), + }; + log.attributes.insert( + "parent_span_id".to_owned(), + LogAttribute(span_id.to_string().into()), + ); + } + } + + if let Some(user) = self.user.as_ref() { + if !log.attributes.contains_key("user.id") { + if let Some(id) = user.id.as_ref() { + log.attributes + .insert("user.id".to_owned(), LogAttribute(id.to_owned().into())); + } + } + + if !log.attributes.contains_key("user.name") { + if let Some(name) = user.username.as_ref() { + log.attributes + .insert("user.name".to_owned(), LogAttribute(name.to_owned().into())); + } + } + + if !log.attributes.contains_key("user.email") { + if let Some(email) = user.email.as_ref() { + log.attributes.insert( + "user.email".to_owned(), + LogAttribute(email.to_owned().into()), + ); + } + } + } + } + /// Set the given [`TransactionOrSpan`] as the active span for this scope. pub fn set_span(&mut self, span: Option) { self.span = Arc::new(span); @@ -336,9 +409,38 @@ impl Scope { self.span.as_ref().clone() } + #[allow(unused_variables)] pub(crate) fn update_session_from_event(&self, event: &Event<'static>) { + #[cfg(feature = "release-health")] if let Some(session) = self.session.lock().unwrap().as_mut() { session.update_from_event(event); } } + + pub(crate) fn apply_propagation_context(&self, event: &mut Event<'_>) { + if event.contexts.contains_key("trace") { + return; + } + + let context = TraceContext { + trace_id: self.propagation_context.trace_id, + span_id: self.propagation_context.span_id, + ..Default::default() + }; + event.contexts.insert("trace".into(), context.into()); + } + + /// Returns the headers needed for distributed tracing. + pub fn iter_trace_propagation_headers(&self) -> impl Iterator { + if let Some(span) = self.get_span() { + span.iter_headers() + } else { + let data = SentryTrace::new( + self.propagation_context.trace_id, + self.propagation_context.span_id, + None, + ); + TraceHeadersIter::new(data.to_string()) + } + } } diff --git a/sentry-core/src/session.rs b/sentry-core/src/session.rs index 1b26af74e..e9fd2f298 100644 --- a/sentry-core/src/session.rs +++ b/sentry-core/src/session.rs @@ -2,665 +2,677 @@ //! //! -use std::collections::HashMap; -use std::sync::{Arc, Condvar, Mutex, MutexGuard}; -use std::thread::JoinHandle; -use std::time::{Duration, Instant, SystemTime}; - -use crate::client::TransportArc; -use crate::clientoptions::SessionMode; -use crate::protocol::{ - EnvelopeItem, Event, Level, SessionAggregateItem, SessionAggregates, SessionAttributes, - SessionStatus, SessionUpdate, -}; -use crate::scope::StackLayer; -use crate::types::random_uuid; -use crate::{Client, Envelope}; - -#[derive(Clone, Debug)] -pub struct Session { - client: Arc, - session_update: SessionUpdate<'static>, - started: Instant, - dirty: bool, -} +#[cfg(feature = "release-health")] +pub use session_impl::*; + +#[cfg(feature = "release-health")] +mod session_impl { + + use std::collections::HashMap; + use std::sync::{Arc, Condvar, Mutex, MutexGuard}; + use std::thread::JoinHandle; + use std::time::{Duration, Instant, SystemTime}; + + use crate::client::TransportArc; + use crate::clientoptions::SessionMode; + use crate::protocol::{ + EnvelopeItem, Event, Level, SessionAggregateItem, SessionAggregates, SessionAttributes, + SessionStatus, SessionUpdate, + }; + + use crate::scope::StackLayer; + + use crate::types::random_uuid; + use crate::{Client, Envelope}; + + #[derive(Clone, Debug)] + pub struct Session { + client: Arc, + session_update: SessionUpdate<'static>, + started: Instant, + dirty: bool, + } -impl Drop for Session { - fn drop(&mut self) { - self.close(SessionStatus::Exited); - if self.dirty { - self.client.enqueue_session(self.session_update.clone()); + impl Drop for Session { + fn drop(&mut self) { + self.close(SessionStatus::Exited); + if self.dirty { + self.client.enqueue_session(self.session_update.clone()); + } } } -} -impl Session { - pub fn from_stack(stack: &StackLayer) -> Option { - let client = stack.client.as_ref()?; - let options = client.options(); - let user = stack.scope.user.as_deref(); - let distinct_id = user - .and_then(|user| { - user.id - .as_ref() - .or(user.email.as_ref()) - .or(user.username.as_ref()) - }) - .cloned(); - Some(Self { - client: client.clone(), - session_update: SessionUpdate { - session_id: random_uuid(), - distinct_id, - sequence: None, - timestamp: None, - started: SystemTime::now(), - init: true, - duration: None, - status: SessionStatus::Ok, - errors: 0, - attributes: SessionAttributes { - release: options.release.clone()?, - environment: options.environment.clone(), - ip_address: None, - user_agent: None, + impl Session { + pub fn from_stack(stack: &StackLayer) -> Option { + let client = stack.client.as_ref()?; + let options = client.options(); + let user = stack.scope.user.as_deref(); + let distinct_id = user + .and_then(|user| { + user.id + .as_ref() + .or(user.email.as_ref()) + .or(user.username.as_ref()) + }) + .cloned(); + Some(Self { + client: client.clone(), + session_update: SessionUpdate { + session_id: random_uuid(), + distinct_id, + sequence: None, + timestamp: None, + started: SystemTime::now(), + init: true, + duration: None, + status: SessionStatus::Ok, + errors: 0, + attributes: SessionAttributes { + release: options.release.clone()?, + environment: options.environment.clone(), + ip_address: None, + user_agent: None, + }, }, - }, - started: Instant::now(), - dirty: true, - }) - } - - pub(crate) fn update_from_event(&mut self, event: &Event<'static>) { - if self.session_update.status != SessionStatus::Ok { - // a session that has already transitioned to a "terminal" state - // should not receive any more updates - return; + started: Instant::now(), + dirty: true, + }) } - let mut has_error = event.level >= Level::Error; - let mut is_crash = false; - for exc in &event.exception.values { - has_error = true; - if let Some(mechanism) = &exc.mechanism { - if let Some(false) = mechanism.handled { - is_crash = true; - break; + + pub(crate) fn update_from_event(&mut self, event: &Event<'static>) { + if self.session_update.status != SessionStatus::Ok { + // a session that has already transitioned to a "terminal" state + // should not receive any more updates + return; + } + let mut has_error = event.level >= Level::Error; + let mut is_crash = false; + for exc in &event.exception.values { + has_error = true; + if let Some(mechanism) = &exc.mechanism { + if let Some(false) = mechanism.handled { + is_crash = true; + break; + } } } - } - if is_crash { - self.session_update.status = SessionStatus::Crashed; - } - if has_error { - self.session_update.errors += 1; - self.dirty = true; + if is_crash { + self.session_update.status = SessionStatus::Crashed; + } + if has_error { + self.session_update.errors += 1; + self.dirty = true; + } } - } - pub(crate) fn close(&mut self, status: SessionStatus) { - if self.session_update.status == SessionStatus::Ok { - let status = match status { - SessionStatus::Ok => SessionStatus::Exited, - s => s, - }; - self.session_update.duration = Some(self.started.elapsed().as_secs_f64()); - self.session_update.status = status; - self.dirty = true; + pub(crate) fn close(&mut self, status: SessionStatus) { + if self.session_update.status == SessionStatus::Ok { + let status = match status { + SessionStatus::Ok => SessionStatus::Exited, + s => s, + }; + self.session_update.duration = Some(self.started.elapsed().as_secs_f64()); + self.session_update.status = status; + self.dirty = true; + } } - } - pub(crate) fn create_envelope_item(&mut self) -> Option { - if self.dirty { - let item = self.session_update.clone().into(); - self.session_update.init = false; - self.dirty = false; - return Some(item); + pub(crate) fn create_envelope_item(&mut self) -> Option { + if self.dirty { + let item = self.session_update.clone().into(); + self.session_update.init = false; + self.dirty = false; + return Some(item); + } + None } - None } -} - -// as defined here: https://develop.sentry.dev/sdk/envelopes/#size-limits -const MAX_SESSION_ITEMS: usize = 100; -const FLUSH_INTERVAL: Duration = Duration::from_secs(60); -#[derive(Debug, Default)] -struct SessionQueue { - individual: Vec>, - aggregated: Option, -} + // as defined here: https://develop.sentry.dev/sdk/envelopes/#size-limits + const MAX_SESSION_ITEMS: usize = 100; + const FLUSH_INTERVAL: Duration = Duration::from_secs(60); -#[derive(Debug)] -struct AggregatedSessions { - buckets: HashMap, - attributes: SessionAttributes<'static>, -} + #[derive(Debug, Default)] + struct SessionQueue { + individual: Vec>, + aggregated: Option, + } -impl From for EnvelopeItem { - fn from(sessions: AggregatedSessions) -> Self { - let aggregates = sessions - .buckets - .into_iter() - .map(|(key, counts)| SessionAggregateItem { - started: key.started, - distinct_id: key.distinct_id, - exited: counts.exited, - errored: counts.errored, - abnormal: counts.abnormal, - crashed: counts.crashed, - }) - .collect(); + #[derive(Debug)] + struct AggregatedSessions { + buckets: HashMap, + attributes: SessionAttributes<'static>, + } - SessionAggregates { - aggregates, - attributes: sessions.attributes, + impl From for EnvelopeItem { + fn from(sessions: AggregatedSessions) -> Self { + let aggregates = sessions + .buckets + .into_iter() + .map(|(key, counts)| SessionAggregateItem { + started: key.started, + distinct_id: key.distinct_id, + exited: counts.exited, + errored: counts.errored, + abnormal: counts.abnormal, + crashed: counts.crashed, + }) + .collect(); + + SessionAggregates { + aggregates, + attributes: sessions.attributes, + } + .into() } - .into() } -} -#[derive(Debug, PartialEq, Eq, Hash)] -struct AggregationKey { - started: SystemTime, - distinct_id: Option, -} + #[derive(Debug, PartialEq, Eq, Hash)] + struct AggregationKey { + started: SystemTime, + distinct_id: Option, + } -#[derive(Debug, Default)] -struct AggregationCounts { - exited: u32, - errored: u32, - abnormal: u32, - crashed: u32, -} + #[derive(Debug, Default)] + struct AggregationCounts { + exited: u32, + errored: u32, + abnormal: u32, + crashed: u32, + } -/// Background Session Flusher -/// -/// The background flusher queues session updates for delayed batched sending. -/// It has its own background thread that will flush its queue once every -/// `FLUSH_INTERVAL`. -pub(crate) struct SessionFlusher { - transport: TransportArc, - mode: SessionMode, - queue: Arc>, - shutdown: Arc<(Mutex, Condvar)>, - worker: Option>, -} + /// Background Session Flusher + /// + /// The background flusher queues session updates for delayed batched sending. + /// It has its own background thread that will flush its queue once every + /// `FLUSH_INTERVAL`. + pub(crate) struct SessionFlusher { + transport: TransportArc, + mode: SessionMode, + queue: Arc>, + shutdown: Arc<(Mutex, Condvar)>, + worker: Option>, + } -impl SessionFlusher { - /// Creates a new Flusher that will submit envelopes to the given `transport`. - pub fn new(transport: TransportArc, mode: SessionMode) -> Self { - let queue = Arc::new(Mutex::new(Default::default())); - #[allow(clippy::mutex_atomic)] - let shutdown = Arc::new((Mutex::new(false), Condvar::new())); - - let worker_transport = transport.clone(); - let worker_queue = queue.clone(); - let worker_shutdown = shutdown.clone(); - let worker = std::thread::Builder::new() - .name("sentry-session-flusher".into()) - .spawn(move || { - let (lock, cvar) = worker_shutdown.as_ref(); - let mut shutdown = lock.lock().unwrap(); - // check this immediately, in case the main thread is already shutting down - if *shutdown { - return; - } - let mut last_flush = Instant::now(); - loop { - let timeout = FLUSH_INTERVAL - .checked_sub(last_flush.elapsed()) - .unwrap_or_else(|| Duration::from_secs(0)); - shutdown = cvar.wait_timeout(shutdown, timeout).unwrap().0; + impl SessionFlusher { + /// Creates a new Flusher that will submit envelopes to the given `transport`. + pub fn new(transport: TransportArc, mode: SessionMode) -> Self { + let queue = Arc::new(Mutex::new(Default::default())); + #[allow(clippy::mutex_atomic)] + let shutdown = Arc::new((Mutex::new(false), Condvar::new())); + + let worker_transport = transport.clone(); + let worker_queue = queue.clone(); + let worker_shutdown = shutdown.clone(); + let worker = std::thread::Builder::new() + .name("sentry-session-flusher".into()) + .spawn(move || { + let (lock, cvar) = worker_shutdown.as_ref(); + let mut shutdown = lock.lock().unwrap(); + // check this immediately, in case the main thread is already shutting down if *shutdown { return; } - if last_flush.elapsed() < FLUSH_INTERVAL { - continue; + let mut last_flush = Instant::now(); + loop { + let timeout = FLUSH_INTERVAL + .checked_sub(last_flush.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + shutdown = cvar.wait_timeout(shutdown, timeout).unwrap().0; + if *shutdown { + return; + } + if last_flush.elapsed() < FLUSH_INTERVAL { + continue; + } + SessionFlusher::flush_queue_internal( + worker_queue.lock().unwrap(), + &worker_transport, + ); + last_flush = Instant::now(); } - SessionFlusher::flush_queue_internal( - worker_queue.lock().unwrap(), - &worker_transport, - ); - last_flush = Instant::now(); - } - }) - .unwrap(); - - Self { - transport, - mode, - queue, - shutdown, - worker: Some(worker), - } - } - - /// Enqueues a session update for delayed sending. - /// - /// This will aggregate session counts in request mode, for all sessions - /// that were not yet partially sent. - pub fn enqueue(&self, session_update: SessionUpdate<'static>) { - let mut queue = self.queue.lock().unwrap(); - if self.mode == SessionMode::Application || !session_update.init { - queue.individual.push(session_update); - if queue.individual.len() >= MAX_SESSION_ITEMS { - SessionFlusher::flush_queue_internal(queue, &self.transport); + }) + .unwrap(); + + Self { + transport, + mode, + queue, + shutdown, + worker: Some(worker), } - return; } - let aggregate = queue.aggregated.get_or_insert_with(|| AggregatedSessions { - buckets: HashMap::with_capacity(1), - attributes: session_update.attributes.clone(), - }); - - let duration = session_update - .started - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap(); - let duration = (duration.as_secs() / 60) * 60; - let started = SystemTime::UNIX_EPOCH - .checked_add(Duration::from_secs(duration)) - .unwrap(); - - let key = AggregationKey { - started, - distinct_id: session_update.distinct_id, - }; - - let bucket = aggregate.buckets.entry(key).or_default(); - - match session_update.status { - SessionStatus::Exited => { - if session_update.errors > 0 { - bucket.errored += 1; - } else { - bucket.exited += 1; + /// Enqueues a session update for delayed sending. + /// + /// This will aggregate session counts in request mode, for all sessions + /// that were not yet partially sent. + pub fn enqueue(&self, session_update: SessionUpdate<'static>) { + let mut queue = self.queue.lock().unwrap(); + if self.mode == SessionMode::Application || !session_update.init { + queue.individual.push(session_update); + if queue.individual.len() >= MAX_SESSION_ITEMS { + SessionFlusher::flush_queue_internal(queue, &self.transport); } + return; } - SessionStatus::Crashed => { - bucket.crashed += 1; - } - SessionStatus::Abnormal => { - bucket.abnormal += 1; - } - SessionStatus::Ok => { - sentry_debug!("unreachable: only closed sessions will be enqueued"); - } - } - } - /// Flushes the queue to the transport. - pub fn flush(&self) { - let queue = self.queue.lock().unwrap(); - SessionFlusher::flush_queue_internal(queue, &self.transport); - } + let aggregate = queue.aggregated.get_or_insert_with(|| AggregatedSessions { + buckets: HashMap::with_capacity(1), + attributes: session_update.attributes.clone(), + }); - /// Flushes the queue to the transport. - /// - /// This is a static method as it will be called from both the background - /// thread and the main thread on drop. - fn flush_queue_internal(mut queue_lock: MutexGuard, transport: &TransportArc) { - let queue = std::mem::take(&mut queue_lock.individual); - let aggregate = queue_lock.aggregated.take(); - drop(queue_lock); - - // send aggregates - if let Some(aggregate) = aggregate { - if let Some(ref transport) = *transport.read().unwrap() { - let mut envelope = Envelope::new(); - envelope.add_item(aggregate); - transport.send_envelope(envelope); + let duration = session_update + .started + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let duration = (duration.as_secs() / 60) * 60; + let started = SystemTime::UNIX_EPOCH + .checked_add(Duration::from_secs(duration)) + .unwrap(); + + let key = AggregationKey { + started, + distinct_id: session_update.distinct_id, + }; + + let bucket = aggregate.buckets.entry(key).or_default(); + + match session_update.status { + SessionStatus::Exited => { + if session_update.errors > 0 { + bucket.errored += 1; + } else { + bucket.exited += 1; + } + } + SessionStatus::Crashed => { + bucket.crashed += 1; + } + SessionStatus::Abnormal => { + bucket.abnormal += 1; + } + SessionStatus::Ok => { + sentry_debug!("unreachable: only closed sessions will be enqueued"); + } } } - // send individual items - if queue.is_empty() { - return; + /// Flushes the queue to the transport. + pub fn flush(&self) { + let queue = self.queue.lock().unwrap(); + SessionFlusher::flush_queue_internal(queue, &self.transport); } - let mut envelope = Envelope::new(); - let mut items = 0; - - for session_update in queue { - if items >= MAX_SESSION_ITEMS { + /// Flushes the queue to the transport. + /// + /// This is a static method as it will be called from both the background + /// thread and the main thread on drop. + fn flush_queue_internal( + mut queue_lock: MutexGuard, + transport: &TransportArc, + ) { + let queue = std::mem::take(&mut queue_lock.individual); + let aggregate = queue_lock.aggregated.take(); + drop(queue_lock); + + // send aggregates + if let Some(aggregate) = aggregate { if let Some(ref transport) = *transport.read().unwrap() { + let mut envelope = Envelope::new(); + envelope.add_item(aggregate); transport.send_envelope(envelope); } - envelope = Envelope::new(); - items = 0; } - envelope.add_item(session_update); - items += 1; - } + // send individual items + if queue.is_empty() { + return; + } - if let Some(ref transport) = *transport.read().unwrap() { - transport.send_envelope(envelope); - } - } -} + let mut envelope = Envelope::new(); + let mut items = 0; + + for session_update in queue { + if items >= MAX_SESSION_ITEMS { + if let Some(ref transport) = *transport.read().unwrap() { + transport.send_envelope(envelope); + } + envelope = Envelope::new(); + items = 0; + } -impl Drop for SessionFlusher { - fn drop(&mut self) { - let (lock, cvar) = self.shutdown.as_ref(); - *lock.lock().unwrap() = true; - cvar.notify_one(); + envelope.add_item(session_update); + items += 1; + } - if let Some(worker) = self.worker.take() { - worker.join().ok(); + if let Some(ref transport) = *transport.read().unwrap() { + transport.send_envelope(envelope); + } } - SessionFlusher::flush_queue_internal(self.queue.lock().unwrap(), &self.transport); } -} -#[cfg(all(test, feature = "test"))] -mod tests { - use std::cmp::Ordering; - - use super::*; - use crate as sentry; - use crate::protocol::{Envelope, EnvelopeItem, SessionStatus}; - - fn capture_envelopes(f: F) -> Vec - where - F: FnOnce(), - { - crate::test::with_captured_envelopes_options( - f, - crate::ClientOptions { - release: Some("some-release".into()), - ..Default::default() - }, - ) - } + impl Drop for SessionFlusher { + fn drop(&mut self) { + let (lock, cvar) = self.shutdown.as_ref(); + *lock.lock().unwrap() = true; + cvar.notify_one(); - #[test] - fn test_session_startstop() { - let envelopes = capture_envelopes(|| { - sentry::start_session(); - std::thread::sleep(std::time::Duration::from_millis(10)); - }); - assert_eq!(envelopes.len(), 1); - - let mut items = envelopes[0].items(); - if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { - assert_eq!(session.status, SessionStatus::Exited); - assert!(session.duration.unwrap() > 0.01); - assert_eq!(session.errors, 0); - assert_eq!(session.attributes.release, "some-release"); - assert!(session.init); - } else { - panic!("expected session"); + if let Some(worker) = self.worker.take() { + worker.join().ok(); + } + SessionFlusher::flush_queue_internal(self.queue.lock().unwrap(), &self.transport); } - assert_eq!(items.next(), None); } - #[test] - fn test_session_batching() { - let envelopes = capture_envelopes(|| { - for _ in 0..(MAX_SESSION_ITEMS * 2) { - sentry::start_session(); - } - }); - // we only want *two* envelope for all the sessions - assert_eq!(envelopes.len(), 2); - - let items = envelopes[0].items().chain(envelopes[1].items()); - assert_eq!(items.clone().count(), MAX_SESSION_ITEMS * 2); - for item in items { - assert!(matches!(item, EnvelopeItem::SessionUpdate(_))); + #[cfg(all(test, feature = "test"))] + mod tests { + use std::cmp::Ordering; + + use super::*; + use crate as sentry; + use crate::protocol::{Envelope, EnvelopeItem, SessionStatus}; + + fn capture_envelopes(f: F) -> Vec + where + F: FnOnce(), + { + crate::test::with_captured_envelopes_options( + f, + crate::ClientOptions { + release: Some("some-release".into()), + ..Default::default() + }, + ) } - } - #[test] - fn test_session_aggregation() { - let envelopes = crate::test::with_captured_envelopes_options( - || { + #[test] + fn test_session_startstop() { + let envelopes = capture_envelopes(|| { sentry::start_session(); - let err = "NaN".parse::().unwrap_err(); - sentry::capture_error(&err); + std::thread::sleep(std::time::Duration::from_millis(10)); + }); + assert_eq!(envelopes.len(), 1); + + let mut items = envelopes[0].items(); + if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { + assert_eq!(session.status, SessionStatus::Exited); + assert!(session.duration.unwrap() > 0.01); + assert_eq!(session.errors, 0); + assert_eq!(session.attributes.release, "some-release"); + assert!(session.init); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); + } - for _ in 0..50 { + #[test] + fn test_session_batching() { + let envelopes = capture_envelopes(|| { + for _ in 0..(MAX_SESSION_ITEMS * 2) { sentry::start_session(); } - sentry::end_session(); - - sentry::configure_scope(|scope| { - scope.set_user(Some(sentry::User { - id: Some("foo-bar".into()), - ..Default::default() - })); - scope.add_event_processor(Box::new(|_| None)); - }); + }); + // we only want *two* envelope for all the sessions + assert_eq!(envelopes.len(), 2); - for _ in 0..50 { + let items = envelopes[0].items().chain(envelopes[1].items()); + assert_eq!(items.clone().count(), MAX_SESSION_ITEMS * 2); + for item in items { + assert!(matches!(item, EnvelopeItem::SessionUpdate(_))); + } + } + + #[test] + fn test_session_aggregation() { + let envelopes = crate::test::with_captured_envelopes_options( + || { sentry::start_session(); - } + let err = "NaN".parse::().unwrap_err(); + sentry::capture_error(&err); - // This error will be discarded because of the event processor, - // and session will not be updated. - // Only events dropped due to sampling should update the session. - let err = "NaN".parse::().unwrap_err(); - sentry::capture_error(&err); - }, - crate::ClientOptions { - release: Some("some-release".into()), - session_mode: SessionMode::Request, - ..Default::default() - }, - ); - assert_eq!(envelopes.len(), 2); - - let mut items = envelopes[0].items(); - assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); - assert_eq!(items.next(), None); - - let mut items = envelopes[1].items(); - if let Some(EnvelopeItem::SessionAggregates(aggregate)) = items.next() { - let mut aggregates = aggregate.aggregates.clone(); - assert_eq!(aggregates.len(), 2); - // the order depends on a hashmap and is not stable otherwise - aggregates.sort_by(|a, b| { - a.distinct_id - .partial_cmp(&b.distinct_id) - .unwrap_or(Ordering::Less) - }); + for _ in 0..50 { + sentry::start_session(); + } + sentry::end_session(); - assert_eq!(aggregates[0].distinct_id, None); - assert_eq!(aggregates[0].exited, 50); + sentry::configure_scope(|scope| { + scope.set_user(Some(sentry::User { + id: Some("foo-bar".into()), + ..Default::default() + })); + scope.add_event_processor(Box::new(|_| None)); + }); - assert_eq!(aggregates[1].errored, 0); - assert_eq!(aggregates[1].distinct_id, Some("foo-bar".into())); - assert_eq!(aggregates[1].exited, 50); - } else { - panic!("expected session"); - } - assert_eq!(items.next(), None); - } + for _ in 0..50 { + sentry::start_session(); + } - #[test] - fn test_session_error() { - let envelopes = capture_envelopes(|| { - sentry::start_session(); - - let err = "NaN".parse::().unwrap_err(); - sentry::capture_error(&err); - }); - assert_eq!(envelopes.len(), 2); - - let mut items = envelopes[0].items(); - assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); - if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { - assert_eq!(session.status, SessionStatus::Ok); - assert_eq!(session.errors, 1); - assert_eq!(session.attributes.release, "some-release"); - assert!(session.init); - } else { - panic!("expected session"); - } - assert_eq!(items.next(), None); - - let mut items = envelopes[1].items(); - if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { - assert_eq!(session.status, SessionStatus::Exited); - assert_eq!(session.errors, 1); - assert!(!session.init); - } else { - panic!("expected session"); - } - assert_eq!(items.next(), None); - } + // This error will be discarded because of the event processor, + // and session will not be updated. + // Only events dropped due to sampling should update the session. + let err = "NaN".parse::().unwrap_err(); + sentry::capture_error(&err); + }, + crate::ClientOptions { + release: Some("some-release".into()), + session_mode: SessionMode::Request, + ..Default::default() + }, + ); + assert_eq!(envelopes.len(), 2); + + let mut items = envelopes[0].items(); + assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); + assert_eq!(items.next(), None); + + let mut items = envelopes[1].items(); + if let Some(EnvelopeItem::SessionAggregates(aggregate)) = items.next() { + let mut aggregates = aggregate.aggregates.clone(); + assert_eq!(aggregates.len(), 2); + // the order depends on a hashmap and is not stable otherwise + aggregates.sort_by(|a, b| { + a.distinct_id + .partial_cmp(&b.distinct_id) + .unwrap_or(Ordering::Less) + }); + + assert_eq!(aggregates[0].distinct_id, None); + assert_eq!(aggregates[0].exited, 50); - #[test] - fn test_session_abnormal() { - let envelopes = capture_envelopes(|| { - sentry::start_session(); - sentry::end_session_with_status(SessionStatus::Abnormal); - }); - assert_eq!(envelopes.len(), 1); - - let mut items = envelopes[0].items(); - if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { - assert_eq!(session.status, SessionStatus::Abnormal); - assert!(session.init); - } else { - panic!("expected session"); + assert_eq!(aggregates[1].errored, 0); + assert_eq!(aggregates[1].distinct_id, Some("foo-bar".into())); + assert_eq!(aggregates[1].exited, 50); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); } - assert_eq!(items.next(), None); - } - #[test] - fn test_session_sampled_errors() { - let mut envelopes = crate::test::with_captured_envelopes_options( - || { + #[test] + fn test_session_error() { + let envelopes = capture_envelopes(|| { sentry::start_session(); - for _ in 0..100 { - let err = "NaN".parse::().unwrap_err(); - sentry::capture_error(&err); - } - }, - crate::ClientOptions { - release: Some("some-release".into()), - sample_rate: 0.5, - ..Default::default() - }, - ); - assert!(envelopes.len() > 25); - assert!(envelopes.len() < 75); - - let envelope = envelopes.pop().unwrap(); - let mut items = envelope.items(); - if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { - assert_eq!(session.status, SessionStatus::Exited); - assert_eq!(session.errors, 100); - } else { - panic!("expected session"); + let err = "NaN".parse::().unwrap_err(); + sentry::capture_error(&err); + }); + assert_eq!(envelopes.len(), 2); + + let mut items = envelopes[0].items(); + assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); + if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { + assert_eq!(session.status, SessionStatus::Ok); + assert_eq!(session.errors, 1); + assert_eq!(session.attributes.release, "some-release"); + assert!(session.init); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); + + let mut items = envelopes[1].items(); + if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { + assert_eq!(session.status, SessionStatus::Exited); + assert_eq!(session.errors, 1); + assert!(!session.init); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); + } + + #[test] + fn test_session_abnormal() { + let envelopes = capture_envelopes(|| { + sentry::start_session(); + sentry::end_session_with_status(SessionStatus::Abnormal); + }); + assert_eq!(envelopes.len(), 1); + + let mut items = envelopes[0].items(); + if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { + assert_eq!(session.status, SessionStatus::Abnormal); + assert!(session.init); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); } - assert_eq!(items.next(), None); - } - /// For _user-mode_ sessions, we want to inherit the session for any _new_ - /// Hub that is spawned from the main thread Hub which already has a session - /// attached - #[test] - fn test_inherit_session_from_top() { - let envelopes = capture_envelopes(|| { - sentry::start_session(); + #[test] + fn test_session_sampled_errors() { + let mut envelopes = crate::test::with_captured_envelopes_options( + || { + sentry::start_session(); - let err = "NaN".parse::().unwrap_err(); - sentry::capture_error(&err); + for _ in 0..100 { + let err = "NaN".parse::().unwrap_err(); + sentry::capture_error(&err); + } + }, + crate::ClientOptions { + release: Some("some-release".into()), + sample_rate: 0.5, + ..Default::default() + }, + ); + assert!(envelopes.len() > 25); + assert!(envelopes.len() < 75); + + let envelope = envelopes.pop().unwrap(); + let mut items = envelope.items(); + if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { + assert_eq!(session.status, SessionStatus::Exited); + assert_eq!(session.errors, 100); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); + } - // create a new Hub which should have the same session - let hub = std::sync::Arc::new(sentry::Hub::new_from_top(sentry::Hub::current())); + /// For _user-mode_ sessions, we want to inherit the session for any _new_ + /// Hub that is spawned from the main thread Hub which already has a session + /// attached + #[test] + fn test_inherit_session_from_top() { + let envelopes = capture_envelopes(|| { + sentry::start_session(); - sentry::Hub::run(hub, || { let err = "NaN".parse::().unwrap_err(); sentry::capture_error(&err); - sentry::with_scope( - |_| {}, - || { - let err = "NaN".parse::().unwrap_err(); - sentry::capture_error(&err); - }, - ); + // create a new Hub which should have the same session + let hub = std::sync::Arc::new(sentry::Hub::new_from_top(sentry::Hub::current())); + + sentry::Hub::run(hub, || { + let err = "NaN".parse::().unwrap_err(); + sentry::capture_error(&err); + + sentry::with_scope( + |_| {}, + || { + let err = "NaN".parse::().unwrap_err(); + sentry::capture_error(&err); + }, + ); + }); }); - }); - assert_eq!(envelopes.len(), 4); // 3 errors and one session end + assert_eq!(envelopes.len(), 4); // 3 errors and one session end - let mut items = envelopes[3].items(); - if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { - assert_eq!(session.status, SessionStatus::Exited); - assert_eq!(session.errors, 3); - assert!(!session.init); - } else { - panic!("expected session"); + let mut items = envelopes[3].items(); + if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { + assert_eq!(session.status, SessionStatus::Exited); + assert_eq!(session.errors, 3); + assert!(!session.init); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); } - assert_eq!(items.next(), None); - } - /// We want to forward-inherit sessions as the previous test asserted, but - /// not *backwards*. So any new session created in a derived Hub and scope - /// will only get updates from that particular scope. - #[test] - fn test_dont_inherit_session_backwards() { - let envelopes = capture_envelopes(|| { - let hub = std::sync::Arc::new(sentry::Hub::new_from_top(sentry::Hub::current())); - - sentry::Hub::run(hub, || { - sentry::with_scope( - |_| {}, - || { - sentry::start_session(); + /// We want to forward-inherit sessions as the previous test asserted, but + /// not *backwards*. So any new session created in a derived Hub and scope + /// will only get updates from that particular scope. + #[test] + fn test_dont_inherit_session_backwards() { + let envelopes = capture_envelopes(|| { + let hub = std::sync::Arc::new(sentry::Hub::new_from_top(sentry::Hub::current())); + + sentry::Hub::run(hub, || { + sentry::with_scope( + |_| {}, + || { + sentry::start_session(); + + let err = "NaN".parse::().unwrap_err(); + sentry::capture_error(&err); + }, + ); - let err = "NaN".parse::().unwrap_err(); - sentry::capture_error(&err); - }, - ); + let err = "NaN".parse::().unwrap_err(); + sentry::capture_error(&err); + }); let err = "NaN".parse::().unwrap_err(); sentry::capture_error(&err); }); - let err = "NaN".parse::().unwrap_err(); - sentry::capture_error(&err); - }); - - assert_eq!(envelopes.len(), 4); // 3 errors and one session end + assert_eq!(envelopes.len(), 4); // 3 errors and one session end - let mut items = envelopes[0].items(); - assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); - if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { - assert_eq!(session.status, SessionStatus::Ok); - assert_eq!(session.errors, 1); - assert!(session.init); - } else { - panic!("expected session"); - } - assert_eq!(items.next(), None); - - // the other two events should not have session updates - let mut items = envelopes[1].items(); - assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); - assert_eq!(items.next(), None); - - let mut items = envelopes[2].items(); - assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); - assert_eq!(items.next(), None); - - // the session end is sent last as it is possibly batched - let mut items = envelopes[3].items(); - if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { - assert_eq!(session.status, SessionStatus::Exited); - assert_eq!(session.errors, 1); - assert!(!session.init); - } else { - panic!("expected session"); + let mut items = envelopes[0].items(); + assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); + if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { + assert_eq!(session.status, SessionStatus::Ok); + assert_eq!(session.errors, 1); + assert!(session.init); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); + + // the other two events should not have session updates + let mut items = envelopes[1].items(); + assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); + assert_eq!(items.next(), None); + + let mut items = envelopes[2].items(); + assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); + assert_eq!(items.next(), None); + + // the session end is sent last as it is possibly batched + let mut items = envelopes[3].items(); + if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { + assert_eq!(session.status, SessionStatus::Exited); + assert_eq!(session.errors, 1); + assert!(!session.init); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); } - assert_eq!(items.next(), None); } } diff --git a/sentry-core/src/test.rs b/sentry-core/src/test.rs index ed92c1c48..a2e150f8e 100644 --- a/sentry-core/src/test.rs +++ b/sentry-core/src/test.rs @@ -19,15 +19,14 @@ //! assert_eq!(events[0].message.as_ref().unwrap(), "Hello World!"); //! ``` -use std::sync::{Arc, Mutex}; - -use once_cell::sync::Lazy; +use std::sync::{Arc, LazyLock, Mutex}; use crate::protocol::Event; use crate::types::Dsn; use crate::{ClientOptions, Envelope, Hub, Transport}; -static TEST_DSN: Lazy = Lazy::new(|| "https://public@sentry.invalid/1".parse().unwrap()); +static TEST_DSN: LazyLock = + LazyLock::new(|| "https://public@sentry.invalid/1".parse().unwrap()); /// Collects events instead of sending them. /// diff --git a/sentry-core/src/units.rs b/sentry-core/src/units.rs deleted file mode 100644 index bc47af659..000000000 --- a/sentry-core/src/units.rs +++ /dev/null @@ -1,260 +0,0 @@ -//! Type definitions for Sentry metrics. - -use std::borrow::Cow; -use std::fmt; - -/// The unit of measurement of a metric value. -/// -/// Units augment metric values by giving them a magnitude and semantics. There are certain types of -/// units that are subdivided in their precision: -/// - [`DurationUnit`]: time durations -/// - [`InformationUnit`]: sizes of information -/// - [`FractionUnit`]: fractions -/// -/// You are not restricted to these units, but can use any `&'static str` or `String` as a unit. -#[derive(Clone, Debug, Eq, PartialEq, Hash, Default)] -pub enum MetricUnit { - /// A time duration, defaulting to `"millisecond"`. - Duration(DurationUnit), - /// Size of information derived from bytes, defaulting to `"byte"`. - Information(InformationUnit), - /// Fractions such as percentages, defaulting to `"ratio"`. - Fraction(FractionUnit), - /// user-defined units without builtin conversion or default. - Custom(Cow<'static, str>), - /// Untyped value without a unit (`""`). - #[default] - None, -} - -impl MetricUnit { - /// Returns `true` if the metric_unit is [`None`]. - pub fn is_none(&self) -> bool { - matches!(self, Self::None) - } -} - -impl fmt::Display for MetricUnit { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - MetricUnit::Duration(u) => u.fmt(f), - MetricUnit::Information(u) => u.fmt(f), - MetricUnit::Fraction(u) => u.fmt(f), - MetricUnit::Custom(u) => u.fmt(f), - MetricUnit::None => f.write_str("none"), - } - } -} - -impl std::str::FromStr for MetricUnit { - type Err = ParseMetricUnitError; - - fn from_str(s: &str) -> Result { - Ok(match s { - "nanosecond" | "ns" => Self::Duration(DurationUnit::NanoSecond), - "microsecond" => Self::Duration(DurationUnit::MicroSecond), - "millisecond" | "ms" => Self::Duration(DurationUnit::MilliSecond), - "second" | "s" => Self::Duration(DurationUnit::Second), - "minute" => Self::Duration(DurationUnit::Minute), - "hour" => Self::Duration(DurationUnit::Hour), - "day" => Self::Duration(DurationUnit::Day), - "week" => Self::Duration(DurationUnit::Week), - - "bit" => Self::Information(InformationUnit::Bit), - "byte" => Self::Information(InformationUnit::Byte), - "kilobyte" => Self::Information(InformationUnit::KiloByte), - "kibibyte" => Self::Information(InformationUnit::KibiByte), - "megabyte" => Self::Information(InformationUnit::MegaByte), - "mebibyte" => Self::Information(InformationUnit::MebiByte), - "gigabyte" => Self::Information(InformationUnit::GigaByte), - "gibibyte" => Self::Information(InformationUnit::GibiByte), - "terabyte" => Self::Information(InformationUnit::TeraByte), - "tebibyte" => Self::Information(InformationUnit::TebiByte), - "petabyte" => Self::Information(InformationUnit::PetaByte), - "pebibyte" => Self::Information(InformationUnit::PebiByte), - "exabyte" => Self::Information(InformationUnit::ExaByte), - "exbibyte" => Self::Information(InformationUnit::ExbiByte), - - "ratio" => Self::Fraction(FractionUnit::Ratio), - "percent" => Self::Fraction(FractionUnit::Percent), - - "" | "none" => Self::None, - _ => Self::Custom(s.to_owned().into()), - }) - } -} - -impl From for MetricUnit { - fn from(unit: DurationUnit) -> Self { - Self::Duration(unit) - } -} - -impl From for MetricUnit { - fn from(unit: InformationUnit) -> Self { - Self::Information(unit) - } -} - -impl From for MetricUnit { - fn from(unit: FractionUnit) -> Self { - Self::Fraction(unit) - } -} - -impl From<&'static str> for MetricUnit { - fn from(unit: &'static str) -> Self { - Self::Custom(unit.into()) - } -} - -impl From for MetricUnit { - fn from(unit: String) -> Self { - Self::Custom(unit.into()) - } -} - -impl From> for MetricUnit { - fn from(unit: Cow<'static, str>) -> Self { - Self::Custom(unit) - } -} - -/// Time duration units used in [`MetricUnit::Duration`]. -/// -/// Defaults to `millisecond`. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] -pub enum DurationUnit { - /// Nanosecond (`"nanosecond"`), 10^-9 seconds. - NanoSecond, - /// Microsecond (`"microsecond"`), 10^-6 seconds. - MicroSecond, - /// Millisecond (`"millisecond"`), 10^-3 seconds. - MilliSecond, - /// Full second (`"second"`). - Second, - /// Minute (`"minute"`), 60 seconds. - Minute, - /// Hour (`"hour"`), 3600 seconds. - Hour, - /// Day (`"day"`), 86,400 seconds. - Day, - /// Week (`"week"`), 604,800 seconds. - Week, -} - -impl Default for DurationUnit { - fn default() -> Self { - Self::MilliSecond - } -} - -impl fmt::Display for DurationUnit { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NanoSecond => f.write_str("nanosecond"), - Self::MicroSecond => f.write_str("microsecond"), - Self::MilliSecond => f.write_str("millisecond"), - Self::Second => f.write_str("second"), - Self::Minute => f.write_str("minute"), - Self::Hour => f.write_str("hour"), - Self::Day => f.write_str("day"), - Self::Week => f.write_str("week"), - } - } -} - -/// An error parsing a [`MetricUnit`] or one of its variants. -#[derive(Clone, Copy, Debug)] -pub struct ParseMetricUnitError(()); - -/// Size of information derived from bytes, used in [`MetricUnit::Information`]. -/// -/// Defaults to `byte`. See also [Units of -/// information](https://en.wikipedia.org/wiki/Units_of_information). -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] -pub enum InformationUnit { - /// Bit (`"bit"`), corresponding to 1/8 of a byte. - /// - /// Note that there are computer systems with a different number of bits per byte. - Bit, - /// Byte (`"byte"`). - Byte, - /// Kilobyte (`"kilobyte"`), 10^3 bytes. - KiloByte, - /// Kibibyte (`"kibibyte"`), 2^10 bytes. - KibiByte, - /// Megabyte (`"megabyte"`), 10^6 bytes. - MegaByte, - /// Mebibyte (`"mebibyte"`), 2^20 bytes. - MebiByte, - /// Gigabyte (`"gigabyte"`), 10^9 bytes. - GigaByte, - /// Gibibyte (`"gibibyte"`), 2^30 bytes. - GibiByte, - /// Terabyte (`"terabyte"`), 10^12 bytes. - TeraByte, - /// Tebibyte (`"tebibyte"`), 2^40 bytes. - TebiByte, - /// Petabyte (`"petabyte"`), 10^15 bytes. - PetaByte, - /// Pebibyte (`"pebibyte"`), 2^50 bytes. - PebiByte, - /// Exabyte (`"exabyte"`), 10^18 bytes. - ExaByte, - /// Exbibyte (`"exbibyte"`), 2^60 bytes. - ExbiByte, -} - -impl Default for InformationUnit { - fn default() -> Self { - Self::Byte - } -} - -impl fmt::Display for InformationUnit { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Bit => f.write_str("bit"), - Self::Byte => f.write_str("byte"), - Self::KiloByte => f.write_str("kilobyte"), - Self::KibiByte => f.write_str("kibibyte"), - Self::MegaByte => f.write_str("megabyte"), - Self::MebiByte => f.write_str("mebibyte"), - Self::GigaByte => f.write_str("gigabyte"), - Self::GibiByte => f.write_str("gibibyte"), - Self::TeraByte => f.write_str("terabyte"), - Self::TebiByte => f.write_str("tebibyte"), - Self::PetaByte => f.write_str("petabyte"), - Self::PebiByte => f.write_str("pebibyte"), - Self::ExaByte => f.write_str("exabyte"), - Self::ExbiByte => f.write_str("exbibyte"), - } - } -} - -/// Units of fraction used in [`MetricUnit::Fraction`]. -/// -/// Defaults to `ratio`. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] -pub enum FractionUnit { - /// Floating point fraction of `1`. - Ratio, - /// Ratio expressed as a fraction of `100`. `100%` equals a ratio of `1.0`. - Percent, -} - -impl Default for FractionUnit { - fn default() -> Self { - Self::Ratio - } -} - -impl fmt::Display for FractionUnit { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Ratio => f.write_str("ratio"), - Self::Percent => f.write_str("percent"), - } - } -} diff --git a/sentry-core/src/utils.rs b/sentry-core/src/utils.rs new file mode 100644 index 000000000..8f1333fcd --- /dev/null +++ b/sentry-core/src/utils.rs @@ -0,0 +1,32 @@ +//! Utilities reused across dependant crates and integrations. + +const SENSITIVE_HEADERS_UPPERCASE: &[&str] = &[ + "AUTHORIZATION", + "PROXY_AUTHORIZATION", + "COOKIE", + "SET_COOKIE", + "X_FORWARDED_FOR", + "X_REAL_IP", + "X_API_KEY", +]; + +const PII_REPLACEMENT: &str = "[Filtered]"; + +/// Determines if the HTTP header with the given name shall be considered as potentially carrying +/// sensitive data. +pub fn is_sensitive_header(name: &str) -> bool { + SENSITIVE_HEADERS_UPPERCASE.contains(&name.to_ascii_uppercase().replace("-", "_").as_str()) +} + +/// Scrub PII (username and password) from the given URL. +pub fn scrub_pii_from_url(mut url: url::Url) -> url::Url { + // the set calls will fail and return an error if the URL is relative + // in those cases, just ignore the errors + if !url.username().is_empty() { + let _ = url.set_username(PII_REPLACEMENT); + } + if url.password().is_some() { + let _ = url.set_password(Some(PII_REPLACEMENT)); + } + url +} diff --git a/sentry-debug-images/Cargo.toml b/sentry-debug-images/Cargo.toml index 777490c6b..f5979c6b9 100644 --- a/sentry-debug-images/Cargo.toml +++ b/sentry-debug-images/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "sentry-debug-images" -version = "0.32.2" +version = "0.45.0" authors = ["Sentry "] -license = "Apache-2.0" +license = "MIT" readme = "README.md" repository = "https://github.com/getsentry/sentry-rust" homepage = "https://sentry.io/welcome/" @@ -10,9 +10,8 @@ description = """ Sentry integration that adds the list of loaded libraries to events. """ edition = "2021" -rust-version = "1.73" +rust-version = "1.81" [dependencies] findshlibs = "=0.10.2" -once_cell = "1" -sentry-core = { version = "0.32.2", path = "../sentry-core" } +sentry-core = { version = "0.45.0", path = "../sentry-core" } diff --git a/sentry-debug-images/LICENSE b/sentry-debug-images/LICENSE index d97d9399c..3e4988928 100644 --- a/sentry-debug-images/LICENSE +++ b/sentry-debug-images/LICENSE @@ -1,203 +1,21 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 Functional Software, Inc. dba Sentry (https://sentry.io) - and individual contributors. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2021 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sentry-debug-images/README.md b/sentry-debug-images/README.md index 962f75dcf..fe362e6b3 100644 --- a/sentry-debug-images/README.md +++ b/sentry-debug-images/README.md @@ -22,11 +22,11 @@ let integration = sentry_debug_images::DebugImagesIntegration::new() .filter(|event| event.level >= Level::Warning); ``` -[`Event`]: https://docs.rs/sentry-debug-images/0.32.2/sentry_debug_images/sentry_core::protocol::Event +[`Event`]: https://docs.rs/sentry-debug-images/0.45.0/sentry_debug_images/sentry_core::protocol::Event ## Resources -License: Apache-2.0 +License: MIT - [Discord](https://discord.gg/ez5KZN7) server for project discussions. - Follow [@getsentry](https://twitter.com/getsentry) on Twitter for updates diff --git a/sentry-debug-images/src/integration.rs b/sentry-debug-images/src/integration.rs index 2a60e1713..780304973 100644 --- a/sentry-debug-images/src/integration.rs +++ b/sentry-debug-images/src/integration.rs @@ -1,9 +1,14 @@ use std::borrow::Cow; +use std::sync::LazyLock; -use once_cell::sync::Lazy; use sentry_core::protocol::{DebugMeta, Event}; use sentry_core::{ClientOptions, Integration}; +static DEBUG_META: LazyLock = LazyLock::new(|| DebugMeta { + images: crate::debug_images(), + ..Default::default() +}); + /// The Sentry Debug Images Integration. pub struct DebugImagesIntegration { filter: Box) -> bool + Send + Sync>, @@ -30,6 +35,7 @@ impl DebugImagesIntegration { impl Default for DebugImagesIntegration { fn default() -> Self { + LazyLock::force(&DEBUG_META); Self { filter: Box::new(|_| true), } @@ -56,11 +62,6 @@ impl Integration for DebugImagesIntegration { mut event: Event<'static>, _opts: &ClientOptions, ) -> Option> { - static DEBUG_META: Lazy = Lazy::new(|| DebugMeta { - images: crate::debug_images(), - ..Default::default() - }); - if event.debug_meta.is_empty() && (self.filter)(&event) { event.debug_meta = Cow::Borrowed(&DEBUG_META); } diff --git a/sentry-log/Cargo.toml b/sentry-log/Cargo.toml index 106485764..1dcaa277c 100644 --- a/sentry-log/Cargo.toml +++ b/sentry-log/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "sentry-log" -version = "0.32.2" +version = "0.45.0" authors = ["Sentry "] -license = "Apache-2.0" +license = "MIT" readme = "README.md" repository = "https://github.com/getsentry/sentry-rust" homepage = "https://sentry.io/welcome/" @@ -10,11 +10,16 @@ description = """ Sentry integration for log and env_logger crates. """ edition = "2021" -rust-version = "1.73" +rust-version = "1.81" + +[features] +default = [] +logs = ["sentry-core/logs"] [dependencies] -sentry-core = { version = "0.32.2", path = "../sentry-core" } -log = { version = "0.4.8", features = ["std"] } +sentry-core = { version = "0.45.0", path = "../sentry-core" } +log = { version = "0.4.8", features = ["std", "kv"] } +bitflags = "2.9.4" [dev-dependencies] sentry = { path = "../sentry", default-features = false, features = ["test"] } diff --git a/sentry-log/LICENSE b/sentry-log/LICENSE index d97d9399c..3e4988928 100644 --- a/sentry-log/LICENSE +++ b/sentry-log/LICENSE @@ -1,203 +1,21 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 Functional Software, Inc. dba Sentry (https://sentry.io) - and individual contributors. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2021 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sentry-log/README.md b/sentry-log/README.md index 03865c7c1..2ee252f66 100644 --- a/sentry-log/README.md +++ b/sentry-log/README.md @@ -6,12 +6,23 @@ # Sentry Rust SDK: sentry-log -Adds support for automatic Breadcrumb and Event capturing from logs. +Adds support for automatic Breadcrumb, Event, and Log capturing from `log` records. -The `log` crate is supported in two ways. First, logs can be captured as -breadcrumbs for later. Secondly, error logs can be captured as events to -Sentry. By default anything above `Info` is recorded as a breadcrumb and -anything above `Error` is captured as error event. +The `log` crate is supported in three ways: +- Records can be captured as Sentry events. These are grouped and show up in the Sentry + [issues](https://docs.sentry.io/product/issues/) page, representing high severity issues to be + acted upon. +- Records can be captured as [breadcrumbs](https://docs.sentry.io/product/issues/issue-details/breadcrumbs/). + Breadcrumbs create a trail of what happened prior to an event, and are therefore sent only when + an event is captured, either manually through e.g. `sentry::capture_message` or through integrations + (e.g. the panic integration is enabled (default) and a panic happens). +- Records can be captured as traditional [logs](https://docs.sentry.io/product/explore/logs/) + Logs can be viewed and queried in the Logs explorer. + +By default anything at or above `Info` is recorded as a breadcrumb and +anything at or above `Error` is captured as error event. +Additionally, if the `sentry` crate is used with the `logs` feature flag, anything at or above `Info` +is captured as a [Structured Log](https://docs.sentry.io/product/explore/logs/). ## Examples @@ -41,9 +52,45 @@ let logger = sentry_log::SentryLogger::new().filter(|md| match md.level() { }); ``` +## Sending multiple items to Sentry + +To map a log record to multiple items in Sentry, you can combine multiple log filters +using the bitwise or operator: + +```rust +use sentry_log::LogFilter; + +let logger = sentry_log::SentryLogger::new().filter(|md| match md.level() { + log::Level::Error => LogFilter::Event, + log::Level::Warn => LogFilter::Breadcrumb | LogFilter::Log, + _ => LogFilter::Ignore, +}); +``` + +If you're using a custom record mapper instead of a filter, you can return a `Vec` +from your mapper function to send multiple items to Sentry from a single log record: + +```rust +use sentry_log::{RecordMapping, SentryLogger, event_from_record, breadcrumb_from_record}; + +let logger = SentryLogger::new().mapper(|record| { + match record.level() { + log::Level::Error => { + // Send both an event and a breadcrumb for errors + vec![ + RecordMapping::Event(event_from_record(record)), + RecordMapping::Breadcrumb(breadcrumb_from_record(record)), + ] + } + log::Level::Warn => RecordMapping::Breadcrumb(breadcrumb_from_record(record)).into(), + _ => RecordMapping::Ignore.into(), + } +}); +``` + ## Resources -License: Apache-2.0 +License: MIT - [Discord](https://discord.gg/ez5KZN7) server for project discussions. - Follow [@getsentry](https://twitter.com/getsentry) on Twitter for updates diff --git a/sentry-log/src/converters.rs b/sentry-log/src/converters.rs index df4d13a1f..1a0b12533 100644 --- a/sentry-log/src/converters.rs +++ b/sentry-log/src/converters.rs @@ -1,7 +1,12 @@ -use sentry_core::protocol::Event; +use sentry_core::protocol::{Event, Value}; +#[cfg(feature = "logs")] +use sentry_core::protocol::{Log, LogAttribute, LogLevel}; use sentry_core::{Breadcrumb, Level}; +use std::collections::BTreeMap; +#[cfg(feature = "logs")] +use std::time::SystemTime; -/// Converts a [`log::Level`] to a Sentry [`Level`] +/// Converts a [`log::Level`] to a Sentry [`Level`], used for [`Event`] and [`Breadcrumb`]. pub fn convert_log_level(level: log::Level) -> Level { match level { log::Level::Error => Level::Error, @@ -11,23 +16,109 @@ pub fn convert_log_level(level: log::Level) -> Level { } } +/// Converts a [`log::Level`] to a Sentry [`LogLevel`], used for [`Log`]. +#[cfg(feature = "logs")] +pub fn convert_log_level_to_sentry_log_level(level: log::Level) -> LogLevel { + match level { + log::Level::Error => LogLevel::Error, + log::Level::Warn => LogLevel::Warn, + log::Level::Info => LogLevel::Info, + log::Level::Debug => LogLevel::Debug, + log::Level::Trace => LogLevel::Trace, + } +} + +/// Visitor to extract key-value pairs from log records +#[derive(Default)] +struct AttributeVisitor { + json_values: BTreeMap, +} + +impl AttributeVisitor { + fn record>(&mut self, key: &str, value: T) { + self.json_values.insert(key.to_owned(), value.into()); + } +} + +impl log::kv::VisitSource<'_> for AttributeVisitor { + fn visit_pair( + &mut self, + key: log::kv::Key, + value: log::kv::Value, + ) -> Result<(), log::kv::Error> { + let key = key.as_str(); + + if let Some(value) = value.to_borrowed_str() { + self.record(key, value); + } else if let Some(value) = value.to_u64() { + self.record(key, value); + } else if let Some(value) = value.to_f64() { + self.record(key, value); + } else if let Some(value) = value.to_bool() { + self.record(key, value); + } else { + self.record(key, format!("{value:?}")); + }; + + Ok(()) + } +} + +fn extract_record_attributes(record: &log::Record<'_>) -> AttributeVisitor { + let mut visitor = AttributeVisitor::default(); + let _ = record.key_values().visit(&mut visitor); + visitor +} + /// Creates a [`Breadcrumb`] from a given [`log::Record`]. pub fn breadcrumb_from_record(record: &log::Record<'_>) -> Breadcrumb { + let visitor = extract_record_attributes(record); + Breadcrumb { ty: "log".into(), level: convert_log_level(record.level()), category: Some(record.target().into()), message: Some(record.args().to_string()), + data: visitor.json_values, ..Default::default() } } /// Creates an [`Event`] from a given [`log::Record`]. pub fn event_from_record(record: &log::Record<'_>) -> Event<'static> { + let visitor = extract_record_attributes(record); + let attributes = visitor.json_values; + + let mut contexts = BTreeMap::new(); + + let mut metadata_map = BTreeMap::new(); + metadata_map.insert("logger.target".into(), record.target().into()); + if let Some(module_path) = record.module_path() { + metadata_map.insert("logger.module_path".into(), module_path.into()); + } + if let Some(file) = record.file() { + metadata_map.insert("logger.file".into(), file.into()); + } + if let Some(line) = record.line() { + metadata_map.insert("logger.line".into(), line.into()); + } + contexts.insert( + "Rust Log Metadata".to_string(), + sentry_core::protocol::Context::Other(metadata_map), + ); + + if !attributes.is_empty() { + contexts.insert( + "Rust Log Attributes".to_string(), + sentry_core::protocol::Context::Other(attributes), + ); + } + Event { logger: Some(record.target().into()), level: convert_log_level(record.level()), message: Some(record.args().to_string()), + contexts, ..Default::default() } } @@ -40,3 +131,37 @@ pub fn exception_from_record(record: &log::Record<'_>) -> Event<'static> { // an exception record. event_from_record(record) } + +/// Creates a [`Log`] from a given [`log::Record`]. +#[cfg(feature = "logs")] +pub fn log_from_record(record: &log::Record<'_>) -> Log { + let visitor = extract_record_attributes(record); + + let mut attributes: BTreeMap = visitor + .json_values + .into_iter() + .map(|(key, val)| (key, val.into())) + .collect(); + + attributes.insert("logger.target".into(), record.target().into()); + if let Some(module_path) = record.module_path() { + attributes.insert("logger.module_path".into(), module_path.into()); + } + if let Some(file) = record.file() { + attributes.insert("logger.file".into(), file.into()); + } + if let Some(line) = record.line() { + attributes.insert("logger.line".into(), line.into()); + } + + attributes.insert("sentry.origin".into(), "auto.log.log".into()); + + Log { + level: convert_log_level_to_sentry_log_level(record.level()), + body: format!("{}", record.args()), + trace_id: None, + timestamp: SystemTime::now(), + severity_number: None, + attributes, + } +} diff --git a/sentry-log/src/lib.rs b/sentry-log/src/lib.rs index bbf667767..a01e8479f 100644 --- a/sentry-log/src/lib.rs +++ b/sentry-log/src/lib.rs @@ -1,9 +1,20 @@ -//! Adds support for automatic Breadcrumb and Event capturing from logs. +//! Adds support for automatic Breadcrumb, Event, and Log capturing from `log` records. //! -//! The `log` crate is supported in two ways. First, logs can be captured as -//! breadcrumbs for later. Secondly, error logs can be captured as events to -//! Sentry. By default anything above `Info` is recorded as a breadcrumb and -//! anything above `Error` is captured as error event. +//! The `log` crate is supported in three ways: +//! - Records can be captured as Sentry events. These are grouped and show up in the Sentry +//! [issues](https://docs.sentry.io/product/issues/) page, representing high severity issues to be +//! acted upon. +//! - Records can be captured as [breadcrumbs](https://docs.sentry.io/product/issues/issue-details/breadcrumbs/). +//! Breadcrumbs create a trail of what happened prior to an event, and are therefore sent only when +//! an event is captured, either manually through e.g. `sentry::capture_message` or through integrations +//! (e.g. the panic integration is enabled (default) and a panic happens). +//! - Records can be captured as traditional [logs](https://docs.sentry.io/product/explore/logs/) +//! Logs can be viewed and queried in the Logs explorer. +//! +//! By default anything at or above `Info` is recorded as a breadcrumb and +//! anything at or above `Error` is captured as error event. +//! Additionally, if the `sentry` crate is used with the `logs` feature flag, anything at or above `Info` +//! is captured as a [Structured Log](https://docs.sentry.io/product/explore/logs/). //! //! # Examples //! @@ -32,6 +43,42 @@ //! _ => LogFilter::Ignore, //! }); //! ``` +//! +//! # Sending multiple items to Sentry +//! +//! To map a log record to multiple items in Sentry, you can combine multiple log filters +//! using the bitwise or operator: +//! +//! ``` +//! use sentry_log::LogFilter; +//! +//! let logger = sentry_log::SentryLogger::new().filter(|md| match md.level() { +//! log::Level::Error => LogFilter::Event, +//! log::Level::Warn => LogFilter::Breadcrumb | LogFilter::Log, +//! _ => LogFilter::Ignore, +//! }); +//! ``` +//! +//! If you're using a custom record mapper instead of a filter, you can return a `Vec` +//! from your mapper function to send multiple items to Sentry from a single log record: +//! +//! ``` +//! use sentry_log::{RecordMapping, SentryLogger, event_from_record, breadcrumb_from_record}; +//! +//! let logger = SentryLogger::new().mapper(|record| { +//! match record.level() { +//! log::Level::Error => { +//! // Send both an event and a breadcrumb for errors +//! vec![ +//! RecordMapping::Event(event_from_record(record)), +//! RecordMapping::Breadcrumb(breadcrumb_from_record(record)), +//! ] +//! } +//! log::Level::Warn => RecordMapping::Breadcrumb(breadcrumb_from_record(record)).into(), +//! _ => RecordMapping::Ignore.into(), +//! } +//! }); +//! ``` #![doc(html_favicon_url = "https://sentry-brand.storage.googleapis.com/favicon.ico")] #![doc(html_logo_url = "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png")] diff --git a/sentry-log/src/logger.rs b/sentry-log/src/logger.rs index f0f1e9ab9..f6cabd2a9 100644 --- a/sentry-log/src/logger.rs +++ b/sentry-log/src/logger.rs @@ -1,19 +1,28 @@ use log::Record; use sentry_core::protocol::{Breadcrumb, Event}; +use bitflags::bitflags; + +#[cfg(feature = "logs")] +use crate::converters::log_from_record; use crate::converters::{breadcrumb_from_record, event_from_record, exception_from_record}; -/// The action that Sentry should perform for a [`log::Metadata`]. -#[derive(Debug)] -pub enum LogFilter { - /// Ignore the [`Record`]. - Ignore, - /// Create a [`Breadcrumb`] from this [`Record`]. - Breadcrumb, - /// Create a message [`Event`] from this [`Record`]. - Event, - /// Create an exception [`Event`] from this [`Record`]. - Exception, +bitflags! { + /// The action that Sentry should perform for a [`log::Metadata`]. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct LogFilter: u32 { + /// Ignore the [`Record`]. + const Ignore = 0b0000; + /// Create a [`Breadcrumb`] from this [`Record`]. + const Breadcrumb = 0b0001; + /// Create a message [`Event`] from this [`Record`]. + const Event = 0b0010; + /// Create an exception [`Event`] from this [`Record`]. + const Exception = 0b0100; + /// Create a [`sentry_core::protocol::Log`] from this [`Record`]. + #[cfg(feature = "logs")] + const Log = 0b1000; + } } /// The type of Data Sentry should ingest for a [`log::Record`]. @@ -26,6 +35,15 @@ pub enum RecordMapping { Breadcrumb(Breadcrumb), /// Captures the [`Event`] to Sentry. Event(Event<'static>), + /// Captures the [`sentry_core::protocol::Log`] to Sentry. + #[cfg(feature = "logs")] + Log(sentry_core::protocol::Log), +} + +impl From for Vec { + fn from(mapping: RecordMapping) -> Self { + vec![mapping] + } } /// The default log filter. @@ -34,7 +52,13 @@ pub enum RecordMapping { /// `warning` and `info`, and `debug` and `trace` logs are ignored. pub fn default_filter(metadata: &log::Metadata) -> LogFilter { match metadata.level() { + #[cfg(feature = "logs")] + log::Level::Error => LogFilter::Exception | LogFilter::Log, + #[cfg(not(feature = "logs"))] log::Level::Error => LogFilter::Exception, + #[cfg(feature = "logs")] + log::Level::Warn | log::Level::Info => LogFilter::Breadcrumb | LogFilter::Log, + #[cfg(not(feature = "logs"))] log::Level::Warn | log::Level::Info => LogFilter::Breadcrumb, log::Level::Debug | log::Level::Trace => LogFilter::Ignore, } @@ -65,7 +89,7 @@ pub struct SentryLogger { dest: L, filter: Box) -> LogFilter + Send + Sync>, #[allow(clippy::type_complexity)] - mapper: Option) -> RecordMapping + Send + Sync>>, + mapper: Option) -> Vec + Send + Sync>>, } impl Default for SentryLogger { @@ -111,38 +135,58 @@ impl SentryLogger { /// Sets a custom mapper function. /// /// The mapper is responsible for creating either breadcrumbs or events - /// from [`Record`]s. + /// from [`Record`]s. It can return either a single [`RecordMapping`] or + /// a `Vec` to send multiple items to Sentry from one log record. #[must_use] - pub fn mapper(mut self, mapper: M) -> Self + pub fn mapper(mut self, mapper: M) -> Self where - M: Fn(&Record<'_>) -> RecordMapping + Send + Sync + 'static, + M: Fn(&Record<'_>) -> T + Send + Sync + 'static, + T: Into>, { - self.mapper = Some(Box::new(mapper)); + self.mapper = Some(Box::new(move |record| mapper(record).into())); self } } impl log::Log for SentryLogger { fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { - self.dest.enabled(metadata) || !matches!((self.filter)(metadata), LogFilter::Ignore) + self.dest.enabled(metadata) || !((self.filter)(metadata) == LogFilter::Ignore) } fn log(&self, record: &log::Record<'_>) { - let item: RecordMapping = match &self.mapper { + let items = match &self.mapper { Some(mapper) => mapper(record), - None => match (self.filter)(record.metadata()) { - LogFilter::Ignore => RecordMapping::Ignore, - LogFilter::Breadcrumb => RecordMapping::Breadcrumb(breadcrumb_from_record(record)), - LogFilter::Event => RecordMapping::Event(event_from_record(record)), - LogFilter::Exception => RecordMapping::Event(exception_from_record(record)), - }, + None => { + let filter = (self.filter)(record.metadata()); + let mut items = vec![]; + if filter.contains(LogFilter::Breadcrumb) { + items.push(RecordMapping::Breadcrumb(breadcrumb_from_record(record))); + } + if filter.contains(LogFilter::Event) { + items.push(RecordMapping::Event(event_from_record(record))); + } + if filter.contains(LogFilter::Exception) { + items.push(RecordMapping::Event(exception_from_record(record))); + } + #[cfg(feature = "logs")] + if filter.contains(LogFilter::Log) { + items.push(RecordMapping::Log(log_from_record(record))); + } + items + } }; - match item { - RecordMapping::Ignore => {} - RecordMapping::Breadcrumb(b) => sentry_core::add_breadcrumb(b), - RecordMapping::Event(e) => { - sentry_core::capture_event(e); + for mapping in items { + match mapping { + RecordMapping::Ignore => {} + RecordMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb), + RecordMapping::Event(event) => { + sentry_core::capture_event(event); + } + #[cfg(feature = "logs")] + RecordMapping::Log(log) => { + sentry_core::Hub::with_active(|hub| hub.capture_log(log)) + } } } diff --git a/sentry-opentelemetry/Cargo.toml b/sentry-opentelemetry/Cargo.toml new file mode 100644 index 000000000..548e98afb --- /dev/null +++ b/sentry-opentelemetry/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "sentry-opentelemetry" +version = "0.45.0" +authors = ["Sentry "] +license = "MIT" +readme = "README.md" +repository = "https://github.com/getsentry/sentry-rust" +homepage = "https://sentry.io/welcome/" +description = """ +Sentry integration for OpenTelemetry. +""" +edition = "2021" +rust-version = "1.81" + +[package.metadata.docs.rs] +all-features = true + +[dependencies] +sentry-core = { version = "0.45.0", path = "../sentry-core", features = [ + "client", +] } +opentelemetry = { version = "0.29.0", default-features = false } +opentelemetry_sdk = { version = "0.29.0", default-features = false, features = [ + "trace", +] } + +[dev-dependencies] +sentry = { path = "../sentry", features = ["test", "opentelemetry"] } +sentry-core = { path = "../sentry-core", features = [ "test" ] } +opentelemetry_sdk = { version = "0.29.0", default-features = false, features = [ + "trace", + "testing", +] } diff --git a/sentry-opentelemetry/README.md b/sentry-opentelemetry/README.md new file mode 100644 index 000000000..21533437e --- /dev/null +++ b/sentry-opentelemetry/README.md @@ -0,0 +1,97 @@ +

+ + Sentry + +

+ +# Sentry Rust SDK: sentry-opentelemetry + +OpenTelemetry support for Sentry. + +This integration allows you to capture spans from your existing OpenTelemetry setup and send +them to Sentry, with support for distributed tracing. + +It's assumed that only the [OpenTelemetry tracing +API](https://opentelemetry.io/docs/specs/otel/trace/api/) is used to start/end/modify Spans. +Mixing it with the Sentry tracing API (e.g. `sentry_core::start_transaction(ctx)`) will not +work, as the spans created with the two methods will not be nested properly. + +Capturing events with `sentry::capture_event` will send them to Sentry with the correct +trace and span association. + +## Configuration + +Add the necessary dependencies to your Cargo.toml: + +```toml +[dependencies] +opentelemetry = { version = "0.29.1", features = ["trace"] } +opentelemetry_sdk = { version = "0.29.0", features = ["trace"] } +sentry = { version = "0.38.0", features = ["opentelemetry"] } +``` + +Initialize Sentry with a `traces_sample_rate`, then register the [`SentryPropagator`] and the +[`SentrySpanProcessor`]: + +```rust +use opentelemetry::{ + global, + trace::{TraceContextExt, Tracer}, + KeyValue, +}; +use opentelemetry_sdk::trace::SdkTracerProvider; +use sentry::integrations::opentelemetry as sentry_opentelemetry; + +// Initialize the Sentry SDK +let _guard = sentry::init(( + "https://your-dsn@sentry.io/0", + sentry::ClientOptions { + // Enable capturing of traces; set this a to lower value in production. + // For more sophisticated behavior use a custom + // [`sentry::ClientOptions::traces_sampler`] instead. + // That's the equivalent of a tail sampling processor in OpenTelemetry. + // These options will only affect sampling of the spans that are sent to Sentry, + // not of the underlying OpenTelemetry spans. + traces_sample_rate: 1.0, + debug: true, + ..sentry::ClientOptions::default() + }, +)); + +// Register the Sentry propagator to enable distributed tracing +global::set_text_map_propagator(sentry_opentelemetry::SentryPropagator::new()); + +let tracer_provider = SdkTracerProvider::builder() + // Register the Sentry span processor to send OpenTelemetry spans to Sentry + .with_span_processor(sentry_opentelemetry::SentrySpanProcessor::new()) + .build(); + +global::set_tracer_provider(tracer_provider); +``` + +## Usage + +Use the OpenTelemetry API to create spans. They will be captured by Sentry: + +```rust + +let tracer = global::tracer("tracer"); +// Creates a Sentry span (transaction) with the name set to "example" +tracer.in_span("example", |_| { + // Creates a Sentry child span with the name set to "child" + tracer.in_span("child", |cx| { + // OTEL span attributes are captured as data attributes on the Sentry span + cx.span().set_attribute(KeyValue::new("my", "attribute")); + + // Captures a Sentry error message and associates it with the ongoing child span + sentry::capture_message("Everything is on fire!", sentry::Level::Error); + }); +}); +``` + +## Resources + +License: MIT + +- [Discord](https://discord.gg/ez5KZN7) server for project discussions. +- Follow [@getsentry](https://twitter.com/getsentry) on Twitter for updates diff --git a/sentry-opentelemetry/src/converters.rs b/sentry-opentelemetry/src/converters.rs new file mode 100644 index 000000000..b0c8c326e --- /dev/null +++ b/sentry-opentelemetry/src/converters.rs @@ -0,0 +1,56 @@ +use sentry_core::protocol::{value::Number, SpanId, SpanStatus, TraceId, Value}; + +pub(crate) fn convert_span_id(span_id: &opentelemetry::SpanId) -> SpanId { + span_id.to_bytes().into() +} + +pub(crate) fn convert_trace_id(trace_id: &opentelemetry::TraceId) -> TraceId { + trace_id.to_bytes().into() +} + +pub(crate) fn convert_span_status(status: &opentelemetry::trace::Status) -> SpanStatus { + match status { + opentelemetry::trace::Status::Unset | opentelemetry::trace::Status::Ok => SpanStatus::Ok, + opentelemetry::trace::Status::Error { description } => { + description.parse().unwrap_or(SpanStatus::UnknownError) + } + } +} + +pub(crate) fn convert_span_kind(kind: opentelemetry::trace::SpanKind) -> Value { + format!("{kind:?}").to_lowercase().into() +} + +pub(crate) fn convert_value(value: opentelemetry::Value) -> Value { + match value { + opentelemetry::Value::Bool(x) => Value::Bool(x), + opentelemetry::Value::I64(x) => Value::Number(x.into()), + opentelemetry::Value::F64(x) => Number::from_f64(x) + .map(Value::Number) + .unwrap_or(Value::Null), + opentelemetry::Value::String(x) => Value::String(x.into()), + opentelemetry::Value::Array(arr) => match arr { + opentelemetry::Array::Bool(items) => { + Value::Array(items.iter().map(|x| Value::Bool(*x)).collect()) + } + opentelemetry::Array::I64(items) => Value::Array( + items + .iter() + .map(|x| Value::Number(Number::from(*x))) + .collect(), + ), + opentelemetry::Array::F64(items) => Value::Array( + items + .iter() + .filter_map(|x| Number::from_f64(*x)) + .map(Value::Number) + .collect(), + ), + opentelemetry::Array::String(items) => { + Value::Array(items.iter().map(|x| x.as_str().into()).collect()) + } + _ => Value::Null, // non-exhaustive + }, + _ => Value::Null, // non-exhaustive + } +} diff --git a/sentry-opentelemetry/src/lib.rs b/sentry-opentelemetry/src/lib.rs new file mode 100644 index 000000000..226b7a18b --- /dev/null +++ b/sentry-opentelemetry/src/lib.rs @@ -0,0 +1,94 @@ +//! OpenTelemetry support for Sentry. +//! +//! This integration allows you to capture spans from your existing OpenTelemetry setup and send +//! them to Sentry, with support for distributed tracing. +//! +//! It's assumed that only the [OpenTelemetry tracing +//! API](https://opentelemetry.io/docs/specs/otel/trace/api/) is used to start/end/modify Spans. +//! Mixing it with the Sentry tracing API (e.g. `sentry_core::start_transaction(ctx)`) will not +//! work, as the spans created with the two methods will not be nested properly. +//! +//! Capturing events with `sentry::capture_event` will send them to Sentry with the correct +//! trace and span association. +//! +//! # Configuration +//! +//! Add the necessary dependencies to your Cargo.toml: +//! +//! ```toml +//! [dependencies] +//! opentelemetry = { version = "0.29.1", features = ["trace"] } +//! opentelemetry_sdk = { version = "0.29.0", features = ["trace"] } +//! sentry = { version = "0.38.0", features = ["opentelemetry"] } +//! ``` +//! +//! Initialize Sentry with a `traces_sample_rate`, then register the [`SentryPropagator`] and the +//! [`SentrySpanProcessor`]: +//! +//! ``` +//! use opentelemetry::{ +//! global, +//! trace::{TraceContextExt, Tracer}, +//! KeyValue, +//! }; +//! use opentelemetry_sdk::trace::SdkTracerProvider; +//! use sentry::integrations::opentelemetry as sentry_opentelemetry; +//! +//! // Initialize the Sentry SDK +//! let _guard = sentry::init(( +//! "https://your-dsn@sentry.io/0", +//! sentry::ClientOptions { +//! // Enable capturing of traces; set this a to lower value in production. +//! // For more sophisticated behavior use a custom +//! // [`sentry::ClientOptions::traces_sampler`] instead. +//! // That's the equivalent of a tail sampling processor in OpenTelemetry. +//! // These options will only affect sampling of the spans that are sent to Sentry, +//! // not of the underlying OpenTelemetry spans. +//! traces_sample_rate: 1.0, +//! debug: true, +//! ..sentry::ClientOptions::default() +//! }, +//! )); +//! +//! // Register the Sentry propagator to enable distributed tracing +//! global::set_text_map_propagator(sentry_opentelemetry::SentryPropagator::new()); +//! +//! let tracer_provider = SdkTracerProvider::builder() +//! // Register the Sentry span processor to send OpenTelemetry spans to Sentry +//! .with_span_processor(sentry_opentelemetry::SentrySpanProcessor::new()) +//! .build(); +//! +//! global::set_tracer_provider(tracer_provider); +//! ``` +//! +//! # Usage +//! +//! Use the OpenTelemetry API to create spans. They will be captured by Sentry: +//! +//! ``` +//! # use opentelemetry::{ +//! # global, +//! # trace::{TraceContextExt, Tracer}, +//! # KeyValue, +//! # }; +//! +//! let tracer = global::tracer("tracer"); +//! // Creates a Sentry span (transaction) with the name set to "example" +//! tracer.in_span("example", |_| { +//! // Creates a Sentry child span with the name set to "child" +//! tracer.in_span("child", |cx| { +//! // OTEL span attributes are captured as data attributes on the Sentry span +//! cx.span().set_attribute(KeyValue::new("my", "attribute")); +//! +//! // Captures a Sentry error message and associates it with the ongoing child span +//! sentry::capture_message("Everything is on fire!", sentry::Level::Error); +//! }); +//! }); +//! ``` + +mod converters; +mod processor; +mod propagator; + +pub use processor::*; +pub use propagator::*; diff --git a/sentry-opentelemetry/src/processor.rs b/sentry-opentelemetry/src/processor.rs new file mode 100644 index 000000000..77b143875 --- /dev/null +++ b/sentry-opentelemetry/src/processor.rs @@ -0,0 +1,204 @@ +//! An OpenTelemetry [SpanProcessor](https://opentelemetry.io/docs/specs/otel/trace/sdk/#span-processor) for Sentry. +//! +//! [`SentrySpanProcessor`] allows the Sentry Rust SDK to integrate with OpenTelemetry. +//! It transforms OpenTelemetry spans into Sentry transactions/spans and sends them to Sentry. +//! +//! # Configuration +//! +//! Unless you have no need for distributed tracing, this should be used together with [`crate::propagator::SentryPropagator`]. An example of +//! setting up both is provided in the [crate-level documentation](../). + +use std::collections::HashMap; +use std::sync::{Arc, LazyLock, Mutex}; +use std::time::SystemTime; + +use opentelemetry::global::ObjectSafeSpan; +use opentelemetry::trace::{get_active_span, SpanId}; +use opentelemetry::Context; +use opentelemetry_sdk::error::OTelSdkResult; +use opentelemetry_sdk::trace::{Span, SpanData, SpanProcessor}; + +use opentelemetry_sdk::Resource; +use sentry_core::SentryTrace; +use sentry_core::{TransactionContext, TransactionOrSpan}; + +use crate::converters::{ + convert_span_id, convert_span_kind, convert_span_status, convert_trace_id, convert_value, +}; + +/// A mapping from Sentry span IDs to Sentry spans/transactions. +/// Sentry spans are created with the same SpanId as the corresponding OTEL span, so this is used +/// to track OTEL spans across start/end calls. +type SpanMap = Arc>>; + +static SPAN_MAP: LazyLock = LazyLock::new(|| Arc::new(Mutex::new(HashMap::new()))); + +/// An OpenTelemetry SpanProcessor that converts OTEL spans to Sentry spans/transactions and sends +/// them to Sentry. +#[derive(Debug, Clone)] +pub struct SentrySpanProcessor {} + +impl SentrySpanProcessor { + /// Creates a new `SentrySpanProcessor`. + pub fn new() -> Self { + sentry_core::configure_scope(|scope| { + // Associate Sentry events with the correct span and trace. + // This works as long as all Sentry spans/transactions are managed exclusively through OTEL APIs. + scope.add_event_processor(|mut event| { + get_active_span(|otel_span| { + let span_map = SPAN_MAP.lock().unwrap(); + + let Some(sentry_span) = + span_map.get(&convert_span_id(&otel_span.span_context().span_id())) + else { + return; + }; + + let (span_id, trace_id) = match sentry_span { + TransactionOrSpan::Transaction(transaction) => ( + transaction.get_trace_context().span_id, + transaction.get_trace_context().trace_id, + ), + TransactionOrSpan::Span(span) => { + (span.get_span_id(), span.get_trace_context().trace_id) + } + }; + + if let Some(sentry_core::protocol::Context::Trace(trace_context)) = + event.contexts.get_mut("trace") + { + trace_context.trace_id = trace_id; + trace_context.span_id = span_id; + } else { + event.contexts.insert( + "trace".into(), + sentry_core::protocol::TraceContext { + span_id, + trace_id, + ..Default::default() + } + .into(), + ); + } + }); + Some(event) + }); + }); + Self {} + } +} + +impl Default for SentrySpanProcessor { + /// Creates a default `SentrySpanProcessor`. + fn default() -> Self { + Self::new() + } +} + +impl SpanProcessor for SentrySpanProcessor { + fn on_start(&self, span: &mut Span, ctx: &Context) { + let span_id = span.span_context().span_id(); + let trace_id = span.span_context().trace_id(); + + let mut span_map = SPAN_MAP.lock().unwrap(); + + let mut span_description = String::new(); + let mut span_op = String::new(); + let mut span_start_timestamp = SystemTime::now(); + let mut parent_sentry_span = None; + if let Some(data) = span.exported_data() { + span_description = data.name.to_string(); + span_op = span_description.clone(); // TODO: infer this from OTEL span attributes + span_start_timestamp = data.start_time; + if data.parent_span_id != SpanId::INVALID { + parent_sentry_span = span_map.get(&convert_span_id(&data.parent_span_id)); + }; + } + let span_description = span_description.as_str(); + let span_op = span_op.as_str(); + + let sentry_span = { + if let Some(parent_sentry_span) = parent_sentry_span { + // continue local trace + TransactionOrSpan::Span(parent_sentry_span.start_child_with_details( + span_op, + span_description, + convert_span_id(&span_id), + span_start_timestamp, + )) + } else { + let sentry_ctx = { + if let Some(sentry_trace) = ctx.get::() { + // continue remote trace + TransactionContext::continue_from_sentry_trace( + span_description, + span_op, + sentry_trace, + Some(convert_span_id(&span_id)), + ) + } else { + // start a new trace + TransactionContext::new_with_details( + span_description, + span_op, + convert_trace_id(&trace_id), + Some(convert_span_id(&span_id)), + None, + ) + } + }; + let tx = + sentry_core::start_transaction_with_timestamp(sentry_ctx, span_start_timestamp); + TransactionOrSpan::Transaction(tx) + } + }; + span_map.insert(convert_span_id(&span_id), sentry_span); + } + + fn on_end(&self, data: SpanData) { + let span_id = data.span_context.span_id(); + + let mut span_map = SPAN_MAP.lock().unwrap(); + + let Some(sentry_span) = span_map.remove(&convert_span_id(&span_id)) else { + return; + }; + + // TODO: read OTEL span events and convert them to Sentry breadcrumbs/events + + sentry_span.set_data("otel.kind", convert_span_kind(data.span_kind)); + for attribute in data.attributes { + sentry_span.set_data(attribute.key.as_str(), convert_value(attribute.value)); + } + + // TODO: read OTEL semantic convention span attributes and map them to the appropriate + // Sentry span attributes/context values + + if let TransactionOrSpan::Transaction(transaction) = &sentry_span { + transaction.set_origin("auto.otel"); + } + sentry_span.set_status(convert_span_status(&data.status)); + sentry_span.finish_with_timestamp(data.end_time); + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown(&self) -> OTelSdkResult { + Ok(()) + } + + fn set_resource(&mut self, resource: &Resource) { + sentry_core::configure_scope(|scope| { + let otel_context = sentry_core::protocol::OtelContext { + resource: resource + .iter() + .map(|(key, value)| (key.as_str().into(), convert_value(value.clone()))) + .collect(), + ..Default::default() + }; + scope.set_context("otel", sentry_core::protocol::Context::from(otel_context)); + }); + } +} diff --git a/sentry-opentelemetry/src/propagator.rs b/sentry-opentelemetry/src/propagator.rs new file mode 100644 index 000000000..396f95f7b --- /dev/null +++ b/sentry-opentelemetry/src/propagator.rs @@ -0,0 +1,82 @@ +//! An OpenTelemetry [Propagator](https://opentelemetry.io/docs/specs/otel/context/api-propagators/) for Sentry. +//! +//! [`SentryPropagator`] serves two purposes: +//! - extracts incoming Sentry tracing metadata from incoming traces, and stores it in +//! [`opentelemetry::baggage::Baggage`]. This information can then be used by +//! [`crate::processor::SentrySpanProcessor`] to achieve distributed tracing. +//! - injects Sentry tracing metadata in outgoing traces. This information can be used by +//! downstream Sentry SDKs to achieve distributed tracing. +//! +//! # Configuration +//! +//! This should be used together with [`crate::processor::SentrySpanProcessor`]. An example of +//! setting up both is provided in the [crate-level documentation](../). + +use std::sync::LazyLock; + +use opentelemetry::{ + propagation::{text_map_propagator::FieldIter, Extractor, Injector, TextMapPropagator}, + trace::TraceContextExt, + Context, SpanId, TraceId, +}; +use sentry_core::parse_headers; +use sentry_core::SentryTrace; + +use crate::converters::{convert_span_id, convert_trace_id}; + +const SENTRY_TRACE_KEY: &str = "sentry-trace"; + +// list of headers used in the inject operation +static SENTRY_PROPAGATOR_FIELDS: LazyLock<[String; 1]> = + LazyLock::new(|| [SENTRY_TRACE_KEY.to_owned()]); + +/// An OpenTelemetry Propagator that injects and extracts Sentry's tracing headers to achieve +/// distributed tracing. +#[derive(Debug, Copy, Clone)] +pub struct SentryPropagator {} + +impl SentryPropagator { + /// Creates a new `SentryPropagator` + pub fn new() -> Self { + Self {} + } +} + +impl Default for SentryPropagator { + /// Creates a default `SentryPropagator`. + fn default() -> Self { + Self::new() + } +} + +impl TextMapPropagator for SentryPropagator { + fn inject_context(&self, ctx: &Context, injector: &mut dyn Injector) { + let trace_id = ctx.span().span_context().trace_id(); + let span_id = ctx.span().span_context().span_id(); + let sampled = ctx.span().span_context().is_sampled(); + if trace_id == TraceId::INVALID || span_id == SpanId::INVALID { + return; + } + let sentry_trace = SentryTrace::new( + convert_trace_id(&trace_id), + convert_span_id(&span_id), + Some(sampled), + ); + injector.set(SENTRY_TRACE_KEY, sentry_trace.to_string()); + } + + fn extract_with_context(&self, ctx: &Context, extractor: &dyn Extractor) -> Context { + let keys = extractor.keys(); + let pairs = keys + .iter() + .filter_map(|&key| extractor.get(key).map(|value| (key, value))); + if let Some(sentry_trace) = parse_headers(pairs) { + return ctx.with_value(sentry_trace); + } + ctx.clone() + } + + fn fields(&self) -> FieldIter<'_> { + FieldIter::new(&*SENTRY_PROPAGATOR_FIELDS) + } +} diff --git a/sentry-opentelemetry/tests/associates_event_with_span.rs b/sentry-opentelemetry/tests/associates_event_with_span.rs new file mode 100644 index 000000000..2e7b411f8 --- /dev/null +++ b/sentry-opentelemetry/tests/associates_event_with_span.rs @@ -0,0 +1,83 @@ +mod shared; + +use opentelemetry::{ + global, + trace::{Tracer, TracerProvider}, +}; +use opentelemetry_sdk::trace::SdkTracerProvider; +use sentry_core::protocol::Transaction; +use sentry_opentelemetry::{SentryPropagator, SentrySpanProcessor}; + +#[test] +fn test_associates_event_with_span() { + let transport = shared::init_sentry(1.0); // Sample all spans + + // Set up OpenTelemetry + global::set_text_map_propagator(SentryPropagator::new()); + let tracer_provider = SdkTracerProvider::builder() + .with_span_processor(SentrySpanProcessor::new()) + .build(); + let tracer = tracer_provider.tracer("test".to_string()); + + // Create root span and execute test within it + tracer.in_span("root_span", |_| { + // Create child span and execute within it + tracer.in_span("child_span", |_| { + // Capture an event while the child span is active + sentry::capture_message("Test message", sentry::Level::Error); + }); + }); + + // Capture the event and spans + let envelopes = transport.fetch_and_clear_envelopes(); + + // Find event and transaction + let mut transaction: Option = None; + let mut span_id: Option = None; + + let mut trace_id_from_event: Option = None; + let mut span_id_from_event: Option = None; + + for envelope in &envelopes { + for item in envelope.items() { + match item { + sentry::protocol::EnvelopeItem::Event(event) => { + trace_id_from_event = event.contexts.get("trace").and_then(|c| match c { + sentry::protocol::Context::Trace(trace) => Some(trace.trace_id.to_string()), + _ => unreachable!(), + }); + span_id_from_event = event.contexts.get("trace").and_then(|c| match c { + sentry::protocol::Context::Trace(trace) => Some(trace.span_id.to_string()), + _ => unreachable!(), + }); + } + sentry::protocol::EnvelopeItem::Transaction(tx) => { + transaction = Some(tx.clone()); + tx.spans.iter().for_each(|span| { + span_id = Some(span.span_id.to_string()); + }); + } + _ => (), + } + } + } + + let transaction = transaction.expect("Should have a transaction"); + let span_id = span_id.expect("Transaction should have a child span"); + + let trace_id_from_event = trace_id_from_event.expect("Event should have a trace ID"); + let span_id_from_event = span_id_from_event.expect("Event should have a span ID"); + + // Verify that the transaction ID and span ID in the event match with the transaction and span + assert_eq!( + { + let context = transaction.contexts.get("trace").unwrap().clone(); + match context { + sentry::protocol::Context::Trace(context) => context.trace_id.to_string(), + _ => unreachable!(), + } + }, + trace_id_from_event + ); + assert_eq!(span_id, span_id_from_event); +} diff --git a/sentry-opentelemetry/tests/captures_transaction.rs b/sentry-opentelemetry/tests/captures_transaction.rs new file mode 100644 index 000000000..3c30ec555 --- /dev/null +++ b/sentry-opentelemetry/tests/captures_transaction.rs @@ -0,0 +1,42 @@ +mod shared; + +use opentelemetry::{ + global, + trace::{Tracer, TracerProvider}, +}; +use opentelemetry_sdk::trace::SdkTracerProvider; +use sentry_opentelemetry::{SentryPropagator, SentrySpanProcessor}; + +#[test] +fn test_captures_transaction() { + // Initialize Sentry + let transport = shared::init_sentry(1.0); // Sample all spans + + // Set up OpenTelemetry + global::set_text_map_propagator(SentryPropagator::new()); + let tracer_provider = SdkTracerProvider::builder() + .with_span_processor(SentrySpanProcessor::new()) + .build(); + let tracer = tracer_provider.tracer("test".to_string()); + + // Create and end a root span + tracer.in_span("root_span", |_| { + // Span body is empty, just creating the span + }); + + // Check that data was sent to Sentry + let envelopes = transport.fetch_and_clear_envelopes(); + assert_eq!( + envelopes.len(), + 1, + "Expected one transaction to be sent to Sentry" + ); + + let transaction = envelopes[0].items().next().unwrap(); + match transaction { + sentry::protocol::EnvelopeItem::Transaction(tx) => { + assert_eq!(tx.name.as_deref(), Some("root_span")); + } + unexpected => panic!("Expected transaction, but got {unexpected:#?}"), + } +} diff --git a/sentry-opentelemetry/tests/captures_transaction_with_nested_spans.rs b/sentry-opentelemetry/tests/captures_transaction_with_nested_spans.rs new file mode 100644 index 000000000..551cd3a0a --- /dev/null +++ b/sentry-opentelemetry/tests/captures_transaction_with_nested_spans.rs @@ -0,0 +1,72 @@ +mod shared; + +use opentelemetry::{ + global, + trace::{Status, TraceContextExt, Tracer, TracerProvider}, + KeyValue, +}; +use opentelemetry_sdk::trace::SdkTracerProvider; +use sentry_opentelemetry::{SentryPropagator, SentrySpanProcessor}; + +#[test] +fn test_captures_transaction_with_nested_spans() { + // Initialize Sentry + let transport = shared::init_sentry(1.0); // Sample all spans + + // Set up OpenTelemetry + global::set_text_map_propagator(SentryPropagator::new()); + let tracer_provider = SdkTracerProvider::builder() + .with_span_processor(SentrySpanProcessor::new()) + .build(); + let tracer = tracer_provider.tracer("test".to_string()); + + // Create nested spans using in_span + tracer.in_span("root_span", |_| { + tracer.in_span("child_span", |_| { + tracer.in_span("grandchild_span", |cx| { + // Add some attributes to the grandchild + cx.span() + .set_attribute(KeyValue::new("test.key", "test.value")); + cx.span().set_status(Status::Ok); + }); + }); + }); + + // Check that data was sent to Sentry + let envelopes = transport.fetch_and_clear_envelopes(); + assert_eq!( + envelopes.len(), + 1, + "Expected one transaction to be sent to Sentry" + ); + + let transaction = envelopes[0].items().next().unwrap(); + match transaction { + sentry::protocol::EnvelopeItem::Transaction(tx) => { + assert_eq!(tx.name.as_deref(), Some("root_span")); + assert_eq!(tx.spans.len(), 2); // Should have 2 child spans + + let child_span = tx + .spans + .iter() + .find(|s| s.description.as_deref() == Some("child_span")) + .expect("Child span should exist"); + let grandchild_span = tx + .spans + .iter() + .find(|s| s.description.as_deref() == Some("grandchild_span")) + .expect("Grandchild span should exist"); + + // Get transaction span ID from trace context + let tx_span_id = match &tx.contexts.get("trace") { + Some(sentry::protocol::Context::Trace(trace)) => trace.span_id, + _ => panic!("Missing trace context in transaction"), + }; + + // Check parent-child relationship + assert_eq!(grandchild_span.parent_span_id, Some(child_span.span_id)); + assert_eq!(child_span.parent_span_id, Some(tx_span_id)); + } + unexpected => panic!("Expected transaction, but got {unexpected:#?}"), + } +} diff --git a/sentry-opentelemetry/tests/creates_distributed_trace.rs b/sentry-opentelemetry/tests/creates_distributed_trace.rs new file mode 100644 index 000000000..369e9ceb3 --- /dev/null +++ b/sentry-opentelemetry/tests/creates_distributed_trace.rs @@ -0,0 +1,126 @@ +mod shared; + +use opentelemetry::{ + global, + propagation::TextMapPropagator, + trace::{TraceContextExt, Tracer, TracerProvider}, + Context, +}; +use opentelemetry_sdk::trace::SdkTracerProvider; +use sentry_opentelemetry::{SentryPropagator, SentrySpanProcessor}; +use std::collections::HashMap; + +#[test] +fn test_creates_distributed_trace() { + let transport = shared::init_sentry(1.0); // Sample all spans + + // Set up OpenTelemetry + global::set_text_map_propagator(SentryPropagator::new()); + let tracer_provider = SdkTracerProvider::builder() + .with_span_processor(SentrySpanProcessor::new()) + .build(); + let tracer = tracer_provider.tracer("test".to_string()); + + // We need to store the context to pass between services, so we'll use a mutable variable + let mut headers = HashMap::new(); + let propagator = SentryPropagator::new(); + + // Create a "first service" span and store context in headers + tracer.in_span("first_service", |first_service_ctx| { + // Simulate passing the context to another service by extracting and injecting e.g. HTTP headers + propagator.inject_context(&first_service_ctx, &mut TestInjector(&mut headers)); + }); + + // Now simulate the second service receiving the headers and continuing the trace + let tracer_provider = SdkTracerProvider::builder() + .with_span_processor(SentrySpanProcessor::new()) + .build(); + let tracer = tracer_provider.tracer("test_2".to_string()); + let propagator = SentryPropagator::new(); + let second_service_ctx = + propagator.extract_with_context(&Context::new(), &TestExtractor(&headers)); + + // Create a second service span that continues the trace + // We need to use start_with_context here to connect with the previous context + let second_service_span = tracer.start_with_context("second_service", &second_service_ctx); + let second_service_ctx = second_service_ctx.with_span(second_service_span); + + // End the second service span + second_service_ctx.span().end(); + + // Get both transactions at once + let envelopes = transport.fetch_and_clear_envelopes(); + assert_eq!( + envelopes.len(), + 2, + "Expected two transactions to be sent to Sentry" + ); + + // Find transactions for first and second services + let mut first_tx = None; + let mut second_tx = None; + + for envelope in &envelopes { + let tx = match envelope.items().next().unwrap() { + sentry::protocol::EnvelopeItem::Transaction(tx) => tx.clone(), + unexpected => panic!("Expected transaction, but got {unexpected:#?}"), + }; + + // Determine which service this transaction belongs to based on name + match tx.name.as_deref() { + Some("first_service") => first_tx = Some(tx), + Some("second_service") => second_tx = Some(tx), + name => panic!("Unexpected transaction name: {name:?}"), + } + } + + let first_tx = first_tx.expect("Missing first service transaction"); + let second_tx = second_tx.expect("Missing second service transaction"); + + // Get first service trace ID and span ID + let (first_trace_id, first_span_id) = match &first_tx.contexts.get("trace") { + Some(sentry::protocol::Context::Trace(trace)) => (trace.trace_id, trace.span_id), + _ => panic!("Missing trace context in first transaction"), + }; + + // Get second service trace ID and span ID + let (second_trace_id, second_span_id, second_parent_span_id) = + match &second_tx.contexts.get("trace") { + Some(sentry::protocol::Context::Trace(trace)) => { + (trace.trace_id, trace.span_id, trace.parent_span_id) + } + _ => panic!("Missing trace context in second transaction"), + }; + + // Verify the distributed trace - same trace ID, different span IDs + assert_eq!(first_trace_id, second_trace_id, "Trace IDs should match"); + assert_ne!( + first_span_id, second_span_id, + "Span IDs should be different" + ); + assert_eq!( + second_parent_span_id, + Some(first_span_id), + "Second service's parent span ID should match first service's span ID" + ); +} + +struct TestInjector<'a>(&'a mut HashMap); + +impl opentelemetry::propagation::Injector for TestInjector<'_> { + fn set(&mut self, key: &str, value: String) { + self.0.insert(key.to_string(), value); + } +} + +struct TestExtractor<'a>(&'a HashMap); + +impl opentelemetry::propagation::Extractor for TestExtractor<'_> { + fn get(&self, key: &str) -> Option<&str> { + self.0.get(key).map(|s| s.as_str()) + } + + fn keys(&self) -> Vec<&str> { + self.0.keys().map(|k| k.as_str()).collect() + } +} diff --git a/sentry-opentelemetry/tests/shared.rs b/sentry-opentelemetry/tests/shared.rs new file mode 100644 index 000000000..5189da054 --- /dev/null +++ b/sentry-opentelemetry/tests/shared.rs @@ -0,0 +1,21 @@ +use sentry::{ClientOptions, Hub}; +use sentry_core::test::TestTransport; + +use std::sync::Arc; + +pub fn init_sentry(traces_sample_rate: f32) -> Arc { + let transport = TestTransport::new(); + let options = ClientOptions { + dsn: Some( + "https://test@sentry-opentelemetry.com/test" + .parse() + .unwrap(), + ), + transport: Some(Arc::new(transport.clone())), + sample_rate: 1.0, + traces_sample_rate, + ..ClientOptions::default() + }; + Hub::current().bind_client(Some(Arc::new(options.into()))); + transport +} diff --git a/sentry-panic/Cargo.toml b/sentry-panic/Cargo.toml index 7fc70a03b..40f6cbae7 100644 --- a/sentry-panic/Cargo.toml +++ b/sentry-panic/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "sentry-panic" -version = "0.32.2" +version = "0.45.0" authors = ["Sentry "] -license = "Apache-2.0" +license = "MIT" readme = "README.md" repository = "https://github.com/getsentry/sentry-rust" homepage = "https://sentry.io/welcome/" @@ -10,11 +10,11 @@ description = """ Sentry integration for capturing panics. """ edition = "2021" -rust-version = "1.73" +rust-version = "1.81" [dependencies] -sentry-core = { version = "0.32.2", path = "../sentry-core" } -sentry-backtrace = { version = "0.32.2", path = "../sentry-backtrace" } +sentry-core = { version = "0.45.0", path = "../sentry-core", features = ["client"] } +sentry-backtrace = { version = "0.45.0", path = "../sentry-backtrace" } [dev-dependencies] sentry = { path = "../sentry", default-features = false, features = ["test"] } diff --git a/sentry-panic/LICENSE b/sentry-panic/LICENSE index d97d9399c..3e4988928 100644 --- a/sentry-panic/LICENSE +++ b/sentry-panic/LICENSE @@ -1,203 +1,21 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 Functional Software, Inc. dba Sentry (https://sentry.io) - and individual contributors. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2021 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sentry-panic/README.md b/sentry-panic/README.md index 5d7d7a9f8..6b9860857 100644 --- a/sentry-panic/README.md +++ b/sentry-panic/README.md @@ -24,7 +24,7 @@ let integration = sentry_panic::PanicIntegration::default().add_extractor(|info| ## Resources -License: Apache-2.0 +License: MIT - [Discord](https://discord.gg/ez5KZN7) server for project discussions. - Follow [@getsentry](https://twitter.com/getsentry) on Twitter for updates diff --git a/sentry-panic/src/lib.rs b/sentry-panic/src/lib.rs index 4c00cab1a..87030b3ac 100644 --- a/sentry-panic/src/lib.rs +++ b/sentry-panic/src/lib.rs @@ -19,6 +19,7 @@ #![warn(missing_docs)] #![deny(unsafe_code)] +#[allow(deprecated)] // `PanicHookInfo` is only available in Rust 1.81+. use std::panic::{self, PanicInfo}; use std::sync::Once; @@ -31,6 +32,7 @@ use sentry_core::{ClientOptions, Integration}; /// This panic handler reports panics to Sentry. It also attempts to prevent /// double faults in some cases where it's known to be unsafe to invoke the /// Sentry panic handler. +#[allow(deprecated)] // `PanicHookInfo` is only available in Rust 1.81+. pub fn panic_handler(info: &PanicInfo<'_>) { sentry_core::with_integration(|integration: &PanicIntegration, hub| { hub.capture_event(integration.event_from_panic_info(info)); @@ -40,6 +42,7 @@ pub fn panic_handler(info: &PanicInfo<'_>) { }); } +#[allow(deprecated)] // `PanicHookInfo` is only available in Rust 1.81+. type PanicExtractor = dyn Fn(&PanicInfo<'_>) -> Option> + Send + Sync; /// The Sentry Panic handler Integration. @@ -75,6 +78,7 @@ impl Integration for PanicIntegration { } /// Extract the message of a panic. +#[allow(deprecated)] // `PanicHookInfo` is only available in Rust 1.81+. pub fn message_from_panic_info<'a>(info: &'a PanicInfo<'_>) -> &'a str { match info.payload().downcast_ref::<&'static str>() { Some(s) => s, @@ -93,6 +97,7 @@ impl PanicIntegration { /// Registers a new extractor. #[must_use] + #[allow(deprecated)] // `PanicHookInfo` is only available in Rust 1.81+. pub fn add_extractor(mut self, f: F) -> Self where F: Fn(&PanicInfo<'_>) -> Option> + Send + Sync + 'static, @@ -104,6 +109,7 @@ impl PanicIntegration { /// Creates an event from the given panic info. /// /// The stacktrace is calculated from the current frame. + #[allow(deprecated)] // `PanicHookInfo` is only available in Rust 1.81+. pub fn event_from_panic_info(&self, info: &PanicInfo<'_>) -> Event<'static> { for extractor in &self.extractors { if let Some(event) = extractor(info) { diff --git a/sentry-slog/Cargo.toml b/sentry-slog/Cargo.toml index b5ea736c9..623bf0dff 100644 --- a/sentry-slog/Cargo.toml +++ b/sentry-slog/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "sentry-slog" -version = "0.32.2" +version = "0.45.0" authors = ["Sentry "] -license = "Apache-2.0" +license = "MIT" readme = "README.md" repository = "https://github.com/getsentry/sentry-rust" homepage = "https://sentry.io/welcome/" @@ -10,10 +10,10 @@ description = """ Sentry integration for the slog crate. """ edition = "2021" -rust-version = "1.73" +rust-version = "1.81" [dependencies] -sentry-core = { version = "0.32.2", path = "../sentry-core" } +sentry-core = { version = "0.45.0", path = "../sentry-core" } slog = { version = "2.5.2", features = ["nested-values"] } serde_json = "1.0.46" diff --git a/sentry-slog/LICENSE b/sentry-slog/LICENSE index d97d9399c..3e4988928 100644 --- a/sentry-slog/LICENSE +++ b/sentry-slog/LICENSE @@ -1,203 +1,21 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 Functional Software, Inc. dba Sentry (https://sentry.io) - and individual contributors. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2021 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sentry-slog/README.md b/sentry-slog/README.md index 8f1d28c77..718bb9f28 100644 --- a/sentry-slog/README.md +++ b/sentry-slog/README.md @@ -74,7 +74,7 @@ provided. ## Resources -License: Apache-2.0 +License: MIT - [Discord](https://discord.gg/ez5KZN7) server for project discussions. - Follow [@getsentry](https://twitter.com/getsentry) on Twitter for updates diff --git a/sentry-slog/src/converters.rs b/sentry-slog/src/converters.rs index 8fafadfbe..3dcfbf365 100644 --- a/sentry-slog/src/converters.rs +++ b/sentry-slog/src/converters.rs @@ -22,7 +22,7 @@ macro_rules! impl_into { } }; } -impl<'a> Serializer for MapSerializer<'a> { +impl Serializer for MapSerializer<'_> { fn emit_arguments(&mut self, key: Key, val: &fmt::Arguments) -> slog::Result { self.0.insert(key.into(), val.to_string().into()); Ok(()) diff --git a/sentry-tower/Cargo.toml b/sentry-tower/Cargo.toml index 37a98052c..7b4468a3a 100644 --- a/sentry-tower/Cargo.toml +++ b/sentry-tower/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "sentry-tower" -version = "0.32.2" +version = "0.45.0" authors = ["Sentry "] -license = "Apache-2.0" +license = "MIT" readme = "README.md" repository = "https://github.com/getsentry/sentry-rust" homepage = "https://sentry.io/welcome/" @@ -10,31 +10,32 @@ description = """ Sentry integration for tower-based crates. """ edition = "2021" -rust-version = "1.73" +rust-version = "1.81" [package.metadata.docs.rs] all-features = true [features] +default = [] http = ["dep:http", "pin-project", "url"] axum-matched-path = ["http", "axum/matched-path"] [dependencies] -axum = { version = "0.7", optional = true, default-features = false } +axum = { version = "0.8", optional = true, default-features = false } tower-layer = "0.3" tower-service = "0.3" http = { version = "1.0.0", optional = true } pin-project = { version = "1.0.10", optional = true } -sentry-core = { version = "0.32.2", path = "../sentry-core", default-features = false, features = [ +sentry-core = { version = "0.45.0", path = "../sentry-core", default-features = false, features = [ "client", ] } url = { version = "2.2.2", optional = true } [dev-dependencies] anyhow = "1" -prost = "0.12.3" +prost = "0.13.3" sentry = { path = "../sentry", default-features = false, features = ["test"] } sentry-anyhow = { path = "../sentry-anyhow" } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } -tonic = { version = "0.10.2", features = ["transport"] } -tower = { version = "0.4", features = ["util", "timeout"] } +tonic = { version = "0.12.3", features = ["transport"] } +tower = { version = "0.5.2", features = ["util", "timeout"] } diff --git a/sentry-tower/LICENSE b/sentry-tower/LICENSE index d97d9399c..3e4988928 100644 --- a/sentry-tower/LICENSE +++ b/sentry-tower/LICENSE @@ -1,203 +1,21 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 Functional Software, Inc. dba Sentry (https://sentry.io) - and individual contributors. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2021 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sentry-tower/README.md b/sentry-tower/README.md index 278ff8710..e4a159ce4 100644 --- a/sentry-tower/README.md +++ b/sentry-tower/README.md @@ -103,7 +103,7 @@ feature of the `sentry` crate instead of the `tower` feature. The created transaction will automatically use the request URI as its name. This is sometimes not desirable in case the request URI contains unique IDs or similar. In this case, users should manually override the transaction name -in the request handler using the [`Scope::set_transaction`](https://docs.rs/sentry-tower/0.32.2/sentry_tower/sentry_core::Scope::set_transaction) +in the request handler using the [`Scope::set_transaction`](https://docs.rs/sentry-tower/0.45.0/sentry_tower/sentry_core::Scope::set_transaction) method. When combining both layers, take care of the ordering of both. For example @@ -113,14 +113,27 @@ one, like so: ```rust let layer = tower::ServiceBuilder::new() .layer(sentry_tower::NewSentryLayer::::new_from_top()) - .layer(sentry_tower::SentryHttpLayer::with_transaction()); + .layer(sentry_tower::SentryHttpLayer::new().enable_transaction()); ``` +When using `axum`, either use [`tower::ServiceBuilder`] as shown above, or make sure you +reorder the layers, like so: + +```rust +let app = Router::new() + .route("/", get(handler)) + .layer(sentry_tower::SentryHttpLayer::new().enable_transaction()) + .layer(sentry_tower::NewSentryLayer::::new_from_top()) +``` + +This is because `axum` applies middleware in the opposite order as [`tower::ServiceBuilder`]. +Applying the layers in the wrong order can result in memory leaks. + [`tower::ServiceBuilder`]: https://docs.rs/tower/latest/tower/struct.ServiceBuilder.html ## Resources -License: Apache-2.0 +License: MIT - [Discord](https://discord.gg/ez5KZN7) server for project discussions. - Follow [@getsentry](https://twitter.com/getsentry) on Twitter for updates diff --git a/sentry-tower/src/http.rs b/sentry-tower/src/http.rs index 32fd69025..5b7c2f4eb 100644 --- a/sentry-tower/src/http.rs +++ b/sentry-tower/src/http.rs @@ -4,13 +4,15 @@ use std::pin::Pin; use std::task::{Context, Poll}; use http::{header, uri, Request, Response, StatusCode}; -use sentry_core::protocol; +use pin_project::pinned_drop; +use sentry_core::utils::{is_sensitive_header, scrub_pii_from_url}; +use sentry_core::{protocol, Hub}; use tower_layer::Layer; use tower_service::Service; -/// Tower Layer that logs Http Request Headers. +/// Tower Layer that captures Http Request information. /// -/// The Service created by this Layer can also optionally start a new +/// The Service created by this Layer can optionally start a new /// performance monitoring transaction for each incoming request, /// continuing the trace based on incoming distributed tracing headers. /// @@ -19,35 +21,63 @@ use tower_service::Service; /// or similar. In this case, users should manually override the transaction name /// in the request handler using the [`Scope::set_transaction`](sentry_core::Scope::set_transaction) /// method. +/// +/// By default, the service will filter out potentially sensitive headers from the captured +/// requests. By enabling `with_pii`, you can opt in to capturing all headers instead. #[derive(Clone, Default)] pub struct SentryHttpLayer { start_transaction: bool, + with_pii: bool, } impl SentryHttpLayer { - /// Creates a new Layer that only logs Request Headers. + /// Creates a new Layer that only captures request information. + /// If a client is bound to the main Hub (i.e. the SDK has already been initialized), set `with_pii` based on the `send_default_pii` client option. pub fn new() -> Self { - Self::default() + let mut slf = Self::default(); + Hub::main() + .client() + .inspect(|client| slf.with_pii = client.options().send_default_pii); + slf } /// Creates a new Layer which starts a new performance monitoring transaction /// for each incoming request. + #[deprecated(since = "0.38.0", note = "please use `enable_transaction` instead")] pub fn with_transaction() -> Self { Self { start_transaction: true, + with_pii: false, } } + + /// Enable starting a new performance monitoring transaction for each incoming request. + #[must_use] + pub fn enable_transaction(mut self) -> Self { + self.start_transaction = true; + self + } + + /// Include PII in captured requests. Potentially sensitive headers are not filtered out. + #[must_use] + pub fn enable_pii(mut self) -> Self { + self.with_pii = true; + self + } } -/// Tower Service that logs Http Request Headers. +/// Tower Service that captures Http Request information. /// -/// The Service can also optionally start a new performance monitoring transaction +/// The Service can optionally start a new performance monitoring transaction /// for each incoming request, continuing the trace based on incoming /// distributed tracing headers. +/// +/// If `with_pii` is disabled, sensitive headers will be filtered out. #[derive(Clone)] pub struct SentryHttpService { service: S, start_transaction: bool, + with_pii: bool, } impl Layer for SentryHttpLayer { @@ -57,12 +87,13 @@ impl Layer for SentryHttpLayer { Self::Service { service, start_transaction: self.start_transaction, + with_pii: self.with_pii, } } } /// The Future returned from [`SentryHttpService`]. -#[pin_project::pin_project] +#[pin_project::pin_project(PinnedDrop)] pub struct SentryHttpFuture { on_first_poll: Option<( sentry_core::protocol::Request, @@ -87,8 +118,9 @@ where if let Some((sentry_req, trx_ctx)) = slf.on_first_poll.take() { sentry_core::configure_scope(|scope| { if let Some(trx_ctx) = trx_ctx { - let transaction: sentry_core::TransactionOrSpan = - sentry_core::start_transaction(trx_ctx).into(); + let transaction = sentry_core::start_transaction(trx_ctx); + transaction.set_origin("auto.http.tower"); + let transaction: sentry_core::TransactionOrSpan = transaction.into(); transaction.set_request(sentry_req.clone()); let parent_span = scope.get_span(); scope.set_span(Some(transaction.clone())); @@ -123,6 +155,23 @@ where } } +#[pinned_drop] +impl PinnedDrop for SentryHttpFuture { + fn drop(self: Pin<&mut Self>) { + let slf = self.project(); + + // If the future gets dropped without being polled to completion, + // still finish the transaction to make sure this is not lost. + if let Some((transaction, parent_span)) = slf.transaction.take() { + if transaction.get_status().is_none() { + transaction.set_status(protocol::SpanStatus::Aborted); + } + transaction.finish(); + sentry_core::configure_scope(|scope| scope.set_span(parent_span)); + } + } +} + impl Service> for SentryHttpService where S: Service, Response = Response>, @@ -138,10 +187,12 @@ where fn call(&mut self, request: Request) -> Self::Future { let sentry_req = sentry_core::protocol::Request { method: Some(request.method().to_string()), - url: get_url_from_request(&request), + url: get_url_from_request(&request).map(scrub_pii_from_url), headers: request .headers() .into_iter() + .filter(|(_, value)| !value.is_sensitive()) + .filter(|(header, _)| self.with_pii || !is_sensitive_header(header.as_str())) .map(|(header, value)| { ( header.to_string(), diff --git a/sentry-tower/src/lib.rs b/sentry-tower/src/lib.rs index 4cd9818da..8c40e5b0c 100644 --- a/sentry-tower/src/lib.rs +++ b/sentry-tower/src/lib.rs @@ -126,10 +126,23 @@ //! # type Request = http::Request; //! let layer = tower::ServiceBuilder::new() //! .layer(sentry_tower::NewSentryLayer::::new_from_top()) -//! .layer(sentry_tower::SentryHttpLayer::with_transaction()); +//! .layer(sentry_tower::SentryHttpLayer::new().enable_transaction()); //! # } //! ``` //! +//! When using `axum`, either use [`tower::ServiceBuilder`] as shown above, or make sure you +//! reorder the layers, like so: +//! +//! ```ignore +//! let app = Router::new() +//! .route("/", get(handler)) +//! .layer(sentry_tower::SentryHttpLayer::new().enable_transaction()) +//! .layer(sentry_tower::NewSentryLayer::::new_from_top()) +//! ``` +//! +//! This is because `axum` applies middleware in the opposite order as [`tower::ServiceBuilder`]. +//! Applying the layers in the wrong order can result in memory leaks. +//! //! [`tower::ServiceBuilder`]: https://docs.rs/tower/latest/tower/struct.ServiceBuilder.html #![doc(html_favicon_url = "https://sentry-brand.storage.googleapis.com/favicon.ico")] @@ -196,7 +209,7 @@ where H: Into>, { provider: P, - _hub: PhantomData<(H, Request)>, + _hub: PhantomData<(H, fn() -> Request)>, } impl Layer for SentryLayer @@ -250,7 +263,7 @@ where { service: S, provider: P, - _hub: PhantomData<(H, Request)>, + _hub: PhantomData<(H, fn() -> Request)>, } impl Service for SentryService @@ -326,3 +339,21 @@ impl NewSentryService { } } } + +#[cfg(test)] +mod tests { + use super::*; + use std::rc::Rc; + + fn assert_sync() {} + + #[test] + fn test_layer_is_sync_when_request_isnt() { + assert_sync::>>(); // Rc<()> is not Sync + } + + #[test] + fn test_service_is_sync_when_request_isnt() { + assert_sync::>>(); // Rc<()> is not Sync + } +} diff --git a/sentry-tracing/Cargo.toml b/sentry-tracing/Cargo.toml index 16493e637..c88854632 100644 --- a/sentry-tracing/Cargo.toml +++ b/sentry-tracing/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "sentry-tracing" -version = "0.32.2" +version = "0.45.0" authors = ["Sentry "] -license = "Apache-2.0" +license = "MIT" readme = "README.md" repository = "https://github.com/getsentry/sentry-rust" homepage = "https://sentry.io/welcome/" @@ -10,7 +10,7 @@ description = """ Sentry integration for tracing and tracing-subscriber crates. """ edition = "2021" -rust-version = "1.73" +rust-version = "1.81" [package.metadata.docs.rs] all-features = true @@ -18,20 +18,23 @@ all-features = true [features] default = [] backtrace = ["dep:sentry-backtrace"] +logs = ["sentry-core/logs"] [dependencies] -sentry-core = { version = "0.32.2", path = "../sentry-core", features = [ +sentry-core = { version = "0.45.0", path = "../sentry-core", features = [ "client", ] } tracing-core = "0.1" -tracing-subscriber = { version = "0.3.1", default-features = false, features = [ +tracing-subscriber = { version = "0.3.20", default-features = false, features = [ "std", ] } -sentry-backtrace = { version = "0.32.2", path = "../sentry-backtrace", optional = true } +sentry-backtrace = { version = "0.45.0", path = "../sentry-backtrace", optional = true } +bitflags = "2.9.4" [dev-dependencies] log = "0.4" -sentry = { path = "../sentry", default-features = false, features = ["test"] } +sentry = { path = "../sentry", default-features = false, features = ["test", "tracing"] } +serde_json = "1" tracing = "0.1" -tracing-subscriber = { version = "0.3.1", features = ["fmt", "registry"] } -tokio = { version = "1.8", features = ["rt-multi-thread", "macros", "time"] } +tracing-subscriber = { version = "0.3.20", features = ["fmt", "registry"] } +tokio = { version = "1.44", features = ["rt-multi-thread", "macros", "time"] } diff --git a/sentry-tracing/LICENSE b/sentry-tracing/LICENSE index d97d9399c..3e4988928 100644 --- a/sentry-tracing/LICENSE +++ b/sentry-tracing/LICENSE @@ -1,203 +1,21 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 Functional Software, Inc. dba Sentry (https://sentry.io) - and individual contributors. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2021 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sentry-tracing/README.md b/sentry-tracing/README.md index 4d12211ee..cd04aa4a1 100644 --- a/sentry-tracing/README.md +++ b/sentry-tracing/README.md @@ -6,13 +6,26 @@ # Sentry Rust SDK: sentry-tracing -Support for automatic breadcrumb, event, and trace capturing from `tracing` events. - -The `tracing` crate is supported in three ways. First, events can be captured as breadcrumbs for -later. Secondly, error events can be captured as events to Sentry. Finally, spans can be -recorded as structured transaction events. By default, events above `Info` are recorded as -breadcrumbs, events above `Error` are captured as error events, and spans above `Info` are -recorded as transactions. +Support for automatic breadcrumb, event, and trace capturing from `tracing` events and spans. + +The `tracing` crate is supported in four ways: +- `tracing` events can be captured as Sentry events. These are grouped and show up in the Sentry + [issues](https://docs.sentry.io/product/issues/) page, representing high severity issues to be + acted upon. +- `tracing` events can be captured as [breadcrumbs](https://docs.sentry.io/product/issues/issue-details/breadcrumbs/). + Breadcrumbs create a trail of what happened prior to an event, and are therefore sent only when + an event is captured, either manually through e.g. `sentry::capture_message` or through integrations + (e.g. the panic integration is enabled (default) and a panic happens). +- `tracing` events can be captured as traditional [structured logs](https://docs.sentry.io/product/explore/logs/). + The `tracing` fields are captured as attributes on the logs, which can be queried in the Logs + explorer. (Available on crate feature `logs`) +- `tracing` spans can be captured as Sentry spans. These can be used to provide more contextual + information for errors, diagnose [performance + issues](https://docs.sentry.io/product/insights/overview/), and capture additional attributes to + aggregate and compute [metrics](https://docs.sentry.io/product/explore/trace-explorer/). + +By default, events above `Info` are recorded as breadcrumbs, events above `Error` are captured +as error events, and spans above `Info` are recorded as spans. ## Configuration @@ -31,21 +44,24 @@ let _guard = sentry::init(sentry::ClientOptions { // Register the Sentry tracing layer to capture breadcrumbs, events, and spans: tracing_subscriber::registry() .with(tracing_subscriber::fmt::layer()) - .with(sentry_tracing::layer()) + .with(sentry::integrations::tracing::layer()) .init(); ``` -It is also possible to set an explicit filter, to customize which log events are captured by -Sentry: +You can customize the behavior of the layer by providing an explicit event filter, to customize which events +are captured by Sentry and the data type they are mapped to. +Similarly, you can provide a span filter to customize which spans are captured by Sentry. ```rust -use sentry_tracing::EventFilter; +use sentry::integrations::tracing::EventFilter; use tracing_subscriber::prelude::*; -let sentry_layer = sentry_tracing::layer().event_filter(|md| match md.level() { - &tracing::Level::ERROR => EventFilter::Event, - _ => EventFilter::Ignore, -}); +let sentry_layer = sentry::integrations::tracing::layer() + .event_filter(|md| match *md.level() { + tracing::Level::ERROR => EventFilter::Event, + _ => EventFilter::Ignore, + }) + .span_filter(|md| matches!(*md.level(), tracing::Level::ERROR | tracing::Level::WARN)); tracing_subscriber::registry() .with(tracing_subscriber::fmt::layer()) @@ -53,7 +69,12 @@ tracing_subscriber::registry() .init(); ``` -## Logging Messages +In addition, a custom event mapper can be provided, to fully customize if and how `tracing` events are converted to Sentry data. + +Note that if both an event mapper and event filter are set, the mapper takes precedence, thus the +filter has no effect. + +## Capturing breadcrumbs Tracing events automatically create breadcrumbs that are attached to the current scope in Sentry. They show up on errors and transactions captured within this scope as shown in the @@ -69,6 +90,23 @@ for i in 0..10 { } ``` +## Capturing logs + +Tracing events can be captured as traditional structured logs in Sentry. +This is gated by the `logs` feature flag and requires setting up a custom Event filter/mapper +to capture logs. You also need to pass `enable_logs: true` in your `sentry::init` call. + +```rust +// assuming `tracing::Level::INFO => EventFilter::Log` in your `event_filter` +for i in 0..10 { + tracing::info!(number = i, my.key = "val", my.num = 42, "This is a log"); +} +``` + +The fields of a `tracing` event are captured as attributes of the log. +Logs can be viewed and queried in the Logs explorer based on message and attributes. +Fields containing dots will be displayed as nested under their common prefix in the UI. + ## Tracking Errors The easiest way to emit errors is by logging an event with `ERROR` level. This will create a @@ -83,7 +121,7 @@ tracing::error!( ); ``` -To track [error structs](https://docs.rs/sentry-tracing/0.32.2/sentry_tracing/std::error::Error), assign a reference to error trait object as field +To track [error structs](https://docs.rs/sentry-tracing/0.45.0/sentry_tracing/std::error::Error), assign a reference to error trait object as field in one of the logging macros. By convention, it is recommended to use the `ERROR` level and assign it to a field called `error`, although the integration will also work with all other levels and field names. @@ -111,10 +149,35 @@ let custom_error = io::Error::new(io::ErrorKind::Other, "oh no"); tracing::error!(error = &custom_error as &dyn Error, "my operation failed"); ``` +## Sending multiple items to Sentry + +To map a `tracing` event to multiple items in Sentry, you can combine multiple event filters +using the bitwise or operator: + +```rust +use sentry::integrations::tracing::EventFilter; +use tracing_subscriber::prelude::*; + +let sentry_layer = sentry::integrations::tracing::layer() + .event_filter(|md| match *md.level() { + tracing::Level::ERROR => EventFilter::Event | EventFilter::Log, + tracing::Level::TRACE => EventFilter::Ignore, + _ => EventFilter::Log, + }) + .span_filter(|md| matches!(*md.level(), tracing::Level::ERROR | tracing::Level::WARN)); + +tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .with(sentry_layer) + .init(); +``` + +If you're using a custom event mapper instead of an event filter, use `EventMapping::Combined`. + ## Tracing Spans The integration automatically tracks `tracing` spans as spans in Sentry. A convenient way to do -this is with the `#[instrument]` attribute macro, which creates a transaction for the function +this is with the `#[instrument]` attribute macro, which creates a span/transaction for the function in Sentry. Function arguments are added as context fields automatically, which can be configured through @@ -125,8 +188,8 @@ use std::time::Duration; use tracing_subscriber::prelude::*; -// Functions instrumented by tracing automatically report -// their span as transactions. +// Functions instrumented by tracing automatically +// create spans/transactions around their execution. #[tracing::instrument] async fn outer() { for i in 0..10 { @@ -144,9 +207,45 @@ async fn inner(i: u32) { } ``` +By default, the name of the span sent to Sentry matches the name of the `tracing` span, which +is the name of the function when using `tracing::instrument`, or the name passed to the +`tracing::_span` macros. + +By default, the `op` of the span sent to Sentry is `default`. + +### Special Span Fields + +Some fields on spans are treated specially by the Sentry tracing integration: +- `sentry.name`: overrides the span name sent to Sentry. + This is useful to customize the span name when using `#[tracing::instrument]`, or to update + it retroactively (using `span.record`) after the span has been created. +- `sentry.op`: overrides the span `op` sent to Sentry. +- `sentry.trace`: in Sentry, the `sentry-trace` header is sent with HTTP requests to achieve distributed tracing. + If the value of this field is set to the value of a valid `sentry-trace` header, which + other Sentry SDKs send automatically with outgoing requests, then the SDK will continue the trace using the given distributed tracing information. + This is useful to achieve distributed tracing at service boundaries by using only the + `tracing` API. + Note that `sentry.trace` will only be effective on span creation (it cannot be applied retroactively) + and requires the span it's applied to to be a root span, i.e. no span should active upon its + creation. + + +Example: + +```rust +#[tracing::instrument(skip_all, fields( + sentry.name = "GET /payments", + sentry.op = "http.server", + sentry.trace = headers.get("sentry-trace").unwrap_or(&"".to_owned()), +))] +async fn handle_request(headers: std::collections::HashMap) { + // ... +} +``` + ## Resources -License: Apache-2.0 +License: MIT - [Discord](https://discord.gg/ez5KZN7) server for project discussions. - Follow [@getsentry](https://twitter.com/getsentry) on Twitter for updates diff --git a/sentry-tracing/src/converters.rs b/sentry-tracing/src/converters.rs index 9f290607d..9ae410946 100644 --- a/sentry-tracing/src/converters.rs +++ b/sentry-tracing/src/converters.rs @@ -1,25 +1,44 @@ use std::collections::BTreeMap; use std::error::Error; -use sentry_core::protocol::{Event, Exception, Mechanism, Thread, Value}; +use sentry_core::protocol::{Event, Exception, Mechanism, Value}; +#[cfg(feature = "logs")] +use sentry_core::protocol::{Log, LogAttribute, LogLevel}; use sentry_core::{event_from_error, Breadcrumb, Level, TransactionOrSpan}; +#[cfg(feature = "logs")] +use std::time::SystemTime; use tracing_core::field::{Field, Visit}; use tracing_core::Subscriber; use tracing_subscriber::layer::Context; use tracing_subscriber::registry::LookupSpan; use super::layer::SentrySpanData; +use crate::TAGS_PREFIX; -/// Converts a [`tracing_core::Level`] to a Sentry [`Level`] -fn convert_tracing_level(level: &tracing_core::Level) -> Level { - match level { - &tracing_core::Level::TRACE | &tracing_core::Level::DEBUG => Level::Debug, - &tracing_core::Level::INFO => Level::Info, - &tracing_core::Level::WARN => Level::Warning, - &tracing_core::Level::ERROR => Level::Error, +/// Converts a [`tracing_core::Level`] to a Sentry [`Level`], used for events and breadcrumbs. +fn level_to_sentry_level(level: &tracing_core::Level) -> Level { + match *level { + tracing_core::Level::TRACE | tracing_core::Level::DEBUG => Level::Debug, + tracing_core::Level::INFO => Level::Info, + tracing_core::Level::WARN => Level::Warning, + tracing_core::Level::ERROR => Level::Error, } } +/// Converts a [`tracing_core::Level`] to a Sentry [`LogLevel`], used for logs. +#[cfg(feature = "logs")] +fn level_to_log_level(level: &tracing_core::Level) -> LogLevel { + match *level { + tracing_core::Level::TRACE => LogLevel::Trace, + tracing_core::Level::DEBUG => LogLevel::Debug, + tracing_core::Level::INFO => LogLevel::Info, + tracing_core::Level::WARN => LogLevel::Warn, + tracing_core::Level::ERROR => LogLevel::Error, + } +} + +/// Converts a [`tracing_core::Level`] to the corresponding Sentry [`Exception::ty`] entry. +#[allow(unused)] fn level_to_exception_type(level: &tracing_core::Level) -> &'static str { match *level { tracing_core::Level::TRACE => "tracing::trace!", @@ -30,11 +49,16 @@ fn level_to_exception_type(level: &tracing_core::Level) -> &'static str { } } -/// Extracts the message and metadata from an event -/// and also optionally from its spans chain. -fn extract_event_data(event: &tracing_core::Event) -> (Option, FieldVisitor) { +/// Extracts the message and metadata from an event. +fn extract_event_data( + event: &tracing_core::Event, + store_errors_in_values: bool, +) -> (Option, FieldVisitor) { // Find message of the event, if any - let mut visitor = FieldVisitor::default(); + let mut visitor = FieldVisitor { + store_errors_in_values, + ..Default::default() + }; event.record(&mut visitor); let message = visitor .json_values @@ -50,14 +74,16 @@ fn extract_event_data(event: &tracing_core::Event) -> (Option, FieldVisi (message, visitor) } +/// Extracts the message and metadata from an event, including the data in the current span. fn extract_event_data_with_context( event: &tracing_core::Event, - ctx: Option>, + ctx: Option<&Context>, + store_errors_in_values: bool, ) -> (Option, FieldVisitor) where S: Subscriber + for<'a> LookupSpan<'a>, { - let (message, mut visitor) = extract_event_data(event); + let (message, mut visitor) = extract_event_data(event, store_errors_in_values); // Add the context fields of every parent span. let current_span = ctx.as_ref().and_then(|ctx| { @@ -70,20 +96,27 @@ where for span in span.scope() { let name = span.name(); let ext = span.extensions(); + if let Some(span_data) = ext.get::() { match &span_data.sentry_span { TransactionOrSpan::Span(span) => { for (key, value) in span.data().iter() { + if is_sentry_span_attribute(key) { + continue; + } if key != "message" { - let key = format!("{}:{}", name, key); + let key = format!("{name}:{key}"); visitor.json_values.insert(key, value.clone()); } } } TransactionOrSpan::Transaction(transaction) => { for (key, value) in transaction.data().iter() { + if is_sentry_span_attribute(key) { + continue; + } if key != "message" { - let key = format!("{}:{}", name, key); + let key = format!("{name}:{key}"); visitor.json_values.insert(key, value.clone()); } } @@ -96,11 +129,23 @@ where (message, visitor) } -/// Records all fields of [`tracing_core::Event`] for easy access +/// Checks whether the given attribute name is one of those set on a span by the Sentry layer. +/// In that case, we want to skip materializing it when propagating attributes, as it would mostly create noise. +fn is_sentry_span_attribute(name: &str) -> bool { + matches!( + name, + "sentry.tracing.target" | "code.module.name" | "code.file.path" | "code.line.number" + ) +} + +/// Records the fields of a [`tracing_core::Event`]. #[derive(Default)] pub(crate) struct FieldVisitor { - pub json_values: BTreeMap, - pub exceptions: Vec, + pub(crate) json_values: BTreeMap, + pub(crate) exceptions: Vec, + /// If `true`, stringify and store errors in `self.json_values` under the original field name + /// else (default), convert to `Exception`s and store in `self.exceptions`. + store_errors_in_values: bool, } impl FieldVisitor { @@ -127,10 +172,20 @@ impl Visit for FieldVisitor { self.record(field, value); } - fn record_error(&mut self, _field: &Field, value: &(dyn Error + 'static)) { + fn record_error(&mut self, field: &Field, value: &(dyn Error + 'static)) { let event = event_from_error(value); - for exception in event.exception { - self.exceptions.push(exception); + if self.store_errors_in_values { + let error_chain = event + .exception + .iter() + .rev() + .filter_map(|x| x.value.as_ref().map(|v| format!("{}: {}", x.ty, *v))) + .collect::>(); + self.record(field, error_chain); + } else { + for exception in event.exception { + self.exceptions.push(exception); + } } } @@ -139,24 +194,32 @@ impl Visit for FieldVisitor { } } -/// Creates a [`Breadcrumb`] from a given [`tracing_core::Event`] -pub fn breadcrumb_from_event(event: &tracing_core::Event) -> Breadcrumb { - let (message, visitor) = extract_event_data(event); +/// Creates a [`Breadcrumb`] from a given [`tracing_core::Event`]. +pub fn breadcrumb_from_event<'context, S>( + event: &tracing_core::Event, + ctx: impl Into>>, +) -> Breadcrumb +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + let (message, visitor) = extract_event_data_with_context(event, ctx.into(), true); + Breadcrumb { category: Some(event.metadata().target().to_owned()), ty: "log".into(), - level: convert_tracing_level(event.metadata().level()), + level: level_to_sentry_level(event.metadata().level()), message, data: visitor.json_values, ..Default::default() } } -fn tags_from_event(fields: &mut BTreeMap) -> BTreeMap { +/// Convert `tracing` fields to the corresponding Sentry tags, removing them from `fields`. +fn extract_and_remove_tags(fields: &mut BTreeMap) -> BTreeMap { let mut tags = BTreeMap::new(); fields.retain(|key, value| { - let Some(key) = key.strip_prefix("tags.") else { + let Some(key) = key.strip_prefix(TAGS_PREFIX) else { return true; }; let string = match value { @@ -177,6 +240,7 @@ fn tags_from_event(fields: &mut BTreeMap) -> BTreeMap, @@ -209,30 +273,10 @@ fn contexts_from_event( context } -/// Creates an [`Event`] from a given [`tracing_core::Event`] +/// Creates an [`Event`] (possibly carrying exceptions) from a given [`tracing_core::Event`]. pub fn event_from_event<'context, S>( event: &tracing_core::Event, - ctx: impl Into>>, -) -> Event<'static> -where - S: Subscriber + for<'a> LookupSpan<'a>, -{ - let (message, mut visitor) = extract_event_data_with_context(event, ctx.into()); - - Event { - logger: Some(event.metadata().target().to_owned()), - level: convert_tracing_level(event.metadata().level()), - message, - tags: tags_from_event(&mut visitor.json_values), - contexts: contexts_from_event(event, visitor.json_values), - ..Default::default() - } -} - -/// Creates an exception [`Event`] from a given [`tracing_core::Event`] -pub fn exception_from_event<'context, S>( - event: &tracing_core::Event, - ctx: impl Into>>, + ctx: impl Into>>, ) -> Event<'static> where S: Subscriber + for<'a> LookupSpan<'a>, @@ -241,57 +285,97 @@ where // proper grouping and issue metadata generation. tracing_core::Record does not contain sufficient // information for this. However, it may contain a serialized error which we can parse to emit // an exception record. - let (mut message, visitor) = extract_event_data_with_context(event, ctx.into()); + #[allow(unused_mut)] + let (mut message, visitor) = extract_event_data_with_context(event, ctx.into(), false); let FieldVisitor { mut exceptions, mut json_values, + store_errors_in_values: _, } = visitor; - // If there are both a message and an exception, then add the message as synthetic wrapper - // around the exception to support proper grouping. If configured, also add the current stack - // trace to this exception directly, since it points to the place where the exception is - // captured. + // If there are a message, an exception, and we are capturing stack traces, then add the message + // as synthetic wrapper around the exception to support proper grouping. The stack trace to + // attach is the current one, since it points to the place where the exception is captured. + // We should only do this if we're capturing stack traces, otherwise the issue title will be `` + // as Sentry will attempt to use missing stack trace to determine the title. + #[cfg(feature = "backtrace")] if !exceptions.is_empty() && message.is_some() { - #[allow(unused_mut)] - let mut thread = Thread::default(); - - #[cfg(feature = "backtrace")] if let Some(client) = sentry_core::Hub::current().client() { if client.options().attach_stacktrace { - thread = sentry_backtrace::current_thread(true); + let thread = sentry_backtrace::current_thread(true); + let exception = Exception { + ty: level_to_exception_type(event.metadata().level()).to_owned(), + value: message.take(), + module: event.metadata().module_path().map(str::to_owned), + stacktrace: thread.stacktrace, + raw_stacktrace: thread.raw_stacktrace, + thread_id: thread.id, + mechanism: Some(Mechanism { + synthetic: Some(true), + ..Mechanism::default() + }), + }; + exceptions.push(exception) } } - - let exception = Exception { - ty: level_to_exception_type(event.metadata().level()).to_owned(), - value: message.take(), - module: event.metadata().module_path().map(str::to_owned), - stacktrace: thread.stacktrace, - raw_stacktrace: thread.raw_stacktrace, - thread_id: thread.id, - mechanism: Some(Mechanism { - synthetic: Some(true), - ..Mechanism::default() - }), - }; - - exceptions.push(exception); } if let Some(exception) = exceptions.last_mut() { - exception - .mechanism - .get_or_insert_with(Mechanism::default) - .ty = "tracing".to_owned(); + "tracing".clone_into( + &mut exception + .mechanism + .get_or_insert_with(Mechanism::default) + .ty, + ); } Event { logger: Some(event.metadata().target().to_owned()), - level: convert_tracing_level(event.metadata().level()), + level: level_to_sentry_level(event.metadata().level()), message, exception: exceptions.into(), - tags: tags_from_event(&mut json_values), + tags: extract_and_remove_tags(&mut json_values), contexts: contexts_from_event(event, json_values), ..Default::default() } } + +/// Creates a [`Log`] from a given [`tracing_core::Event`] +#[cfg(feature = "logs")] +pub fn log_from_event<'context, S>( + event: &tracing_core::Event, + ctx: impl Into>>, +) -> Log +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + let (message, visitor) = extract_event_data_with_context(event, ctx.into(), true); + + let mut attributes: BTreeMap = visitor + .json_values + .into_iter() + .map(|(key, val)| (key, val.into())) + .collect(); + + let event_meta = event.metadata(); + if let Some(module_path) = event_meta.module_path() { + attributes.insert("code.module.name".to_owned(), module_path.into()); + } + if let Some(file) = event_meta.file() { + attributes.insert("code.file.path".to_owned(), file.into()); + } + if let Some(line) = event_meta.line() { + attributes.insert("code.line.number".to_owned(), line.into()); + } + + attributes.insert("sentry.origin".to_owned(), "auto.tracing".into()); + + Log { + level: level_to_log_level(event.metadata().level()), + body: message.unwrap_or_default(), + trace_id: None, + timestamp: SystemTime::now(), + severity_number: None, + attributes, + } +} diff --git a/sentry-tracing/src/layer.rs b/sentry-tracing/src/layer.rs index 8183bbfc0..d02c0496d 100644 --- a/sentry-tracing/src/layer.rs +++ b/sentry-tracing/src/layer.rs @@ -1,6 +1,9 @@ +use std::borrow::Cow; use std::cell::RefCell; use std::collections::BTreeMap; +use std::sync::Arc; +use bitflags::bitflags; use sentry_core::protocol::Value; use sentry_core::{Breadcrumb, TransactionOrSpan}; use tracing_core::field::Visit; @@ -9,21 +12,27 @@ use tracing_subscriber::layer::{Context, Layer}; use tracing_subscriber::registry::LookupSpan; use crate::converters::*; - -/// The action that Sentry should perform for a [`Metadata`] -#[derive(Debug, Clone, Copy)] -pub enum EventFilter { - /// Ignore the [`Event`] - Ignore, - /// Create a [`Breadcrumb`] from this [`Event`] - Breadcrumb, - /// Create a message [`sentry_core::protocol::Event`] from this [`Event`] - Event, - /// Create an exception [`sentry_core::protocol::Event`] from this [`Event`] - Exception, +use crate::SENTRY_NAME_FIELD; +use crate::SENTRY_OP_FIELD; +use crate::SENTRY_TRACE_FIELD; +use crate::TAGS_PREFIX; + +bitflags! { + /// The action that Sentry should perform for a given [`Event`] + #[derive(Debug, Clone, Copy)] + pub struct EventFilter: u32 { + /// Ignore the [`Event`] + const Ignore = 0b000; + /// Create a [`Breadcrumb`] from this [`Event`] + const Breadcrumb = 0b001; + /// Create a [`sentry_core::protocol::Event`] from this [`Event`] + const Event = 0b010; + /// Create a [`sentry_core::protocol::Log`] from this [`Event`] + const Log = 0b100; + } } -/// The type of data Sentry should ingest for a [`Event`] +/// The type of data Sentry should ingest for an [`Event`]. #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum EventMapping { @@ -33,6 +42,31 @@ pub enum EventMapping { Breadcrumb(Breadcrumb), /// Captures the [`sentry_core::protocol::Event`] to Sentry. Event(sentry_core::protocol::Event<'static>), + /// Captures the [`sentry_core::protocol::Log`] to Sentry. + #[cfg(feature = "logs")] + Log(sentry_core::protocol::Log), + /// Captures multiple items to Sentry. + /// Nesting multiple `EventMapping::Combined` inside each other will cause the inner mappings to be ignored. + Combined(CombinedEventMapping), +} + +/// A list of event mappings. +#[derive(Debug)] +pub struct CombinedEventMapping(Vec); + +impl From for CombinedEventMapping { + fn from(value: EventMapping) -> Self { + match value { + EventMapping::Combined(combined) => combined, + _ => CombinedEventMapping(vec![value]), + } + } +} + +impl From> for CombinedEventMapping { + fn from(value: Vec) -> Self { + Self(value) + } } /// The default event filter. @@ -41,7 +75,13 @@ pub enum EventMapping { /// `warning` and `info`, and `debug` and `trace` logs are ignored. pub fn default_event_filter(metadata: &Metadata) -> EventFilter { match metadata.level() { - &Level::ERROR => EventFilter::Exception, + #[cfg(feature = "logs")] + &Level::ERROR => EventFilter::Event | EventFilter::Log, + #[cfg(not(feature = "logs"))] + &Level::ERROR => EventFilter::Event, + #[cfg(feature = "logs")] + &Level::WARN | &Level::INFO => EventFilter::Breadcrumb | EventFilter::Log, + #[cfg(not(feature = "logs"))] &Level::WARN | &Level::INFO => EventFilter::Breadcrumb, &Level::DEBUG | &Level::TRACE => EventFilter::Ignore, } @@ -142,12 +182,61 @@ where } } +#[inline(always)] +fn record_fields<'a, K: AsRef + Into>>( + span: &TransactionOrSpan, + data: BTreeMap, +) { + match span { + TransactionOrSpan::Span(span) => { + let mut span = span.data(); + for (key, value) in data { + if let Some(stripped_key) = key.as_ref().strip_prefix(TAGS_PREFIX) { + match value { + Value::Bool(value) => { + span.set_tag(stripped_key.to_owned(), value.to_string()) + } + Value::Number(value) => { + span.set_tag(stripped_key.to_owned(), value.to_string()) + } + Value::String(value) => span.set_tag(stripped_key.to_owned(), value), + _ => span.set_data(key.into().into_owned(), value), + } + } else { + span.set_data(key.into().into_owned(), value); + } + } + } + TransactionOrSpan::Transaction(transaction) => { + let mut transaction = transaction.data(); + for (key, value) in data { + if let Some(stripped_key) = key.as_ref().strip_prefix(TAGS_PREFIX) { + match value { + Value::Bool(value) => { + transaction.set_tag(stripped_key.into(), value.to_string()) + } + Value::Number(value) => { + transaction.set_tag(stripped_key.into(), value.to_string()) + } + Value::String(value) => transaction.set_tag(stripped_key.into(), value), + _ => transaction.set_data(key.into(), value), + } + } else { + transaction.set_data(key.into(), value); + } + } + } + } +} + /// Data that is attached to the tracing Spans `extensions`, in order to /// `finish` the corresponding sentry span `on_close`, and re-set its parent as /// the *current* span. pub(super) struct SentrySpanData { pub(super) sentry_span: TransactionOrSpan, parent_sentry_span: Option, + hub: Arc, + hub_switch_guard: Option, } impl Layer for SentryLayer @@ -155,29 +244,48 @@ where S: Subscriber + for<'a> LookupSpan<'a>, { fn on_event(&self, event: &Event, ctx: Context<'_, S>) { - let item = match &self.event_mapper { + let items = match &self.event_mapper { Some(mapper) => mapper(event, ctx), None => { let span_ctx = self.with_span_attributes.then_some(ctx); - match (self.event_filter)(event.metadata()) { - EventFilter::Ignore => EventMapping::Ignore, - EventFilter::Breadcrumb => { - EventMapping::Breadcrumb(breadcrumb_from_event(event)) - } - EventFilter::Event => EventMapping::Event(event_from_event(event, span_ctx)), - EventFilter::Exception => { - EventMapping::Event(exception_from_event(event, span_ctx)) - } + let filter = (self.event_filter)(event.metadata()); + let mut items = vec![]; + if filter.contains(EventFilter::Breadcrumb) { + items.push(EventMapping::Breadcrumb(breadcrumb_from_event( + event, + span_ctx.as_ref(), + ))); + } + if filter.contains(EventFilter::Event) { + items.push(EventMapping::Event(event_from_event( + event, + span_ctx.as_ref(), + ))); } + #[cfg(feature = "logs")] + if filter.contains(EventFilter::Log) { + items.push(EventMapping::Log(log_from_event(event, span_ctx.as_ref()))); + } + EventMapping::Combined(CombinedEventMapping(items)) } }; - - match item { - EventMapping::Event(event) => { - sentry_core::capture_event(event); + let items = CombinedEventMapping::from(items); + + for item in items.0 { + match item { + EventMapping::Ignore => (), + EventMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb), + EventMapping::Event(event) => { + sentry_core::capture_event(event); + } + #[cfg(feature = "logs")] + EventMapping::Log(log) => sentry_core::Hub::with_active(|hub| hub.capture_log(log)), + EventMapping::Combined(_) => { + sentry_core::sentry_debug!( + "[SentryLayer] found nested CombinedEventMapping, ignoring" + ) + } } - EventMapping::Breadcrumb(breadcrumb) => sentry_core::add_breadcrumb(breadcrumb), - _ => (), } } @@ -193,43 +301,80 @@ where return; } - let (description, data) = extract_span_data(attrs); - let op = span.name(); - - // Spans don't always have a description, this ensures our data is not empty, - // therefore the Sentry UI will be a lot more valuable for navigating spans. - let description = description.unwrap_or_else(|| { - let target = span.metadata().target(); - if target.is_empty() { - op.to_string() - } else { - format!("{target}::{op}") - } - }); + let (data, sentry_name, sentry_op, sentry_trace) = extract_span_data(attrs); + let sentry_name = sentry_name.as_deref().unwrap_or_else(|| span.name()); + let sentry_op = + sentry_op.unwrap_or_else(|| format!("{}::{}", span.metadata().target(), span.name())); + + let hub = sentry_core::Hub::current(); + let parent_sentry_span = hub.configure_scope(|scope| scope.get_span()); - let parent_sentry_span = sentry_core::configure_scope(|s| s.get_span()); - let sentry_span: sentry_core::TransactionOrSpan = match &parent_sentry_span { - Some(parent) => parent.start_child(op, &description).into(), + let mut sentry_span: sentry_core::TransactionOrSpan = match &parent_sentry_span { + Some(parent) => parent.start_child(&sentry_op, sentry_name).into(), None => { - let ctx = sentry_core::TransactionContext::new(&description, op); - sentry_core::start_transaction(ctx).into() + let ctx = if let Some(trace_header) = sentry_trace { + sentry_core::TransactionContext::continue_from_headers( + sentry_name, + &sentry_op, + [("sentry-trace", trace_header.as_str())], + ) + } else { + sentry_core::TransactionContext::new(sentry_name, &sentry_op) + }; + + let tx = sentry_core::start_transaction(ctx); + tx.set_origin("auto.tracing"); + tx.into() } }; // Add the data from the original span to the sentry span. // This comes from typically the `fields` in `tracing::instrument`. - for (key, value) in data { - sentry_span.set_data(key, value); - } + record_fields(&sentry_span, data); - sentry_core::configure_scope(|scope| scope.set_span(Some(sentry_span.clone()))); + set_default_attributes(&mut sentry_span, span.metadata()); let mut extensions = span.extensions_mut(); extensions.insert(SentrySpanData { sentry_span, parent_sentry_span, + hub, + hub_switch_guard: None, }); } + /// Sets entered span as *current* sentry span. A tracing span can be + /// entered and existed multiple times, for example, when using a `tracing::Instrumented` future. + fn on_enter(&self, id: &span::Id, ctx: Context<'_, S>) { + let span = match ctx.span(id) { + Some(span) => span, + None => return, + }; + + let mut extensions = span.extensions_mut(); + if let Some(data) = extensions.get_mut::() { + data.hub_switch_guard = Some(sentry_core::HubSwitchGuard::new(data.hub.clone())); + data.hub.configure_scope(|scope| { + scope.set_span(Some(data.sentry_span.clone())); + }) + } + } + + /// Set exited span's parent as *current* sentry span. + fn on_exit(&self, id: &span::Id, ctx: Context<'_, S>) { + let span = match ctx.span(id) { + Some(span) => span, + None => return, + }; + + let mut extensions = span.extensions_mut(); + if let Some(data) = extensions.get_mut::() { + data.hub.configure_scope(|scope| { + scope.set_span(data.parent_sentry_span.clone()); + }); + data.hub_switch_guard.take(); + } + } + /// When a span gets closed, finish the underlying sentry span, and set back /// its parent as the *current* sentry span. fn on_close(&self, id: span::Id, ctx: Context<'_, S>) { @@ -239,16 +384,12 @@ where }; let mut extensions = span.extensions_mut(); - let SentrySpanData { - sentry_span, - parent_sentry_span, - } = match extensions.remove::() { + let SentrySpanData { sentry_span, .. } = match extensions.remove::() { Some(data) => data, None => return, }; sentry_span.finish(); - sentry_core::configure_scope(|scope| scope.set_span(parent_sentry_span)); } /// Implement the writing of extra data to span @@ -267,9 +408,49 @@ where let mut data = FieldVisitor::default(); values.record(&mut data); - for (key, value) in data.json_values { - span.set_data(&key, value); + let sentry_name = data + .json_values + .remove(SENTRY_NAME_FIELD) + .and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }); + + let sentry_op = data + .json_values + .remove(SENTRY_OP_FIELD) + .and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }); + + // `sentry.trace` cannot be applied retroactively + data.json_values.remove(SENTRY_TRACE_FIELD); + + if let Some(name) = sentry_name { + span.set_name(&name); + } + if let Some(op) = sentry_op { + span.set_op(&op); } + + record_fields(span, data.json_values); + } +} + +fn set_default_attributes(span: &mut TransactionOrSpan, metadata: &Metadata<'_>) { + span.set_data("sentry.tracing.target", metadata.target().into()); + + if let Some(module) = metadata.module_path() { + span.set_data("code.module.name", module.into()); + } + + if let Some(file) = metadata.file() { + span.set_data("code.file.path", file.into()); + } + + if let Some(line) = metadata.line() { + span.set_data("code.line.number", line.into()); } } @@ -281,8 +462,16 @@ where Default::default() } -/// Extracts the message and attributes from a span -fn extract_span_data(attrs: &span::Attributes) -> (Option, BTreeMap<&'static str, Value>) { +/// Extracts the attributes from a span, +/// returning the values of SENTRY_NAME_FIELD, SENTRY_OP_FIELD, SENTRY_TRACE_FIELD separately +fn extract_span_data( + attrs: &span::Attributes, +) -> ( + BTreeMap<&'static str, Value>, + Option, + Option, + Option, +) { let mut json_values = VISITOR_BUFFER.with_borrow_mut(|debug_buffer| { let mut visitor = SpanFieldVisitor { debug_buffer, @@ -292,13 +481,24 @@ fn extract_span_data(attrs: &span::Attributes) -> (Option, BTreeMap<&'st visitor.json_values }); - // Find message of the span, if any - let message = json_values.remove("message").and_then(|v| match v { + let name = json_values.remove(SENTRY_NAME_FIELD).and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }); + + let op = json_values.remove(SENTRY_OP_FIELD).and_then(|v| match v { Value::String(s) => Some(s), _ => None, }); - (message, json_values) + let sentry_trace = json_values + .remove(SENTRY_TRACE_FIELD) + .and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }); + + (json_values, name, op, sentry_trace) } thread_local! { diff --git a/sentry-tracing/src/lib.rs b/sentry-tracing/src/lib.rs index 4f99a12f4..100b8efd4 100644 --- a/sentry-tracing/src/lib.rs +++ b/sentry-tracing/src/lib.rs @@ -1,10 +1,23 @@ -//! Support for automatic breadcrumb, event, and trace capturing from `tracing` events. -//! -//! The `tracing` crate is supported in three ways. First, events can be captured as breadcrumbs for -//! later. Secondly, error events can be captured as events to Sentry. Finally, spans can be -//! recorded as structured transaction events. By default, events above `Info` are recorded as -//! breadcrumbs, events above `Error` are captured as error events, and spans above `Info` are -//! recorded as transactions. +//! Support for automatic breadcrumb, event, and trace capturing from `tracing` events and spans. +//! +//! The `tracing` crate is supported in four ways: +//! - `tracing` events can be captured as Sentry events. These are grouped and show up in the Sentry +//! [issues](https://docs.sentry.io/product/issues/) page, representing high severity issues to be +//! acted upon. +//! - `tracing` events can be captured as [breadcrumbs](https://docs.sentry.io/product/issues/issue-details/breadcrumbs/). +//! Breadcrumbs create a trail of what happened prior to an event, and are therefore sent only when +//! an event is captured, either manually through e.g. `sentry::capture_message` or through integrations +//! (e.g. the panic integration is enabled (default) and a panic happens). +//! - `tracing` events can be captured as traditional [structured logs](https://docs.sentry.io/product/explore/logs/). +//! The `tracing` fields are captured as attributes on the logs, which can be queried in the Logs +//! explorer. (Available on crate feature `logs`) +//! - `tracing` spans can be captured as Sentry spans. These can be used to provide more contextual +//! information for errors, diagnose [performance +//! issues](https://docs.sentry.io/product/insights/overview/), and capture additional attributes to +//! aggregate and compute [metrics](https://docs.sentry.io/product/explore/trace-explorer/). +//! +//! By default, events above `Info` are recorded as breadcrumbs, events above `Error` are captured +//! as error events, and spans above `Info` are recorded as spans. //! //! # Configuration //! @@ -23,21 +36,24 @@ //! // Register the Sentry tracing layer to capture breadcrumbs, events, and spans: //! tracing_subscriber::registry() //! .with(tracing_subscriber::fmt::layer()) -//! .with(sentry_tracing::layer()) +//! .with(sentry::integrations::tracing::layer()) //! .init(); //! ``` //! -//! It is also possible to set an explicit filter, to customize which log events are captured by -//! Sentry: +//! You can customize the behavior of the layer by providing an explicit event filter, to customize which events +//! are captured by Sentry and the data type they are mapped to. +//! Similarly, you can provide a span filter to customize which spans are captured by Sentry. //! //! ``` -//! use sentry_tracing::EventFilter; +//! use sentry::integrations::tracing::EventFilter; //! use tracing_subscriber::prelude::*; //! -//! let sentry_layer = sentry_tracing::layer().event_filter(|md| match md.level() { -//! &tracing::Level::ERROR => EventFilter::Event, -//! _ => EventFilter::Ignore, -//! }); +//! let sentry_layer = sentry::integrations::tracing::layer() +//! .event_filter(|md| match *md.level() { +//! tracing::Level::ERROR => EventFilter::Event, +//! _ => EventFilter::Ignore, +//! }) +//! .span_filter(|md| matches!(*md.level(), tracing::Level::ERROR | tracing::Level::WARN)); //! //! tracing_subscriber::registry() //! .with(tracing_subscriber::fmt::layer()) @@ -45,7 +61,12 @@ //! .init(); //! ``` //! -//! # Logging Messages +//! In addition, a custom event mapper can be provided, to fully customize if and how `tracing` events are converted to Sentry data. +//! +//! Note that if both an event mapper and event filter are set, the mapper takes precedence, thus the +//! filter has no effect. +//! +//! # Capturing breadcrumbs //! //! Tracing events automatically create breadcrumbs that are attached to the current scope in //! Sentry. They show up on errors and transactions captured within this scope as shown in the @@ -61,6 +82,23 @@ //! } //! ``` //! +//! # Capturing logs +//! +//! Tracing events can be captured as traditional structured logs in Sentry. +//! This is gated by the `logs` feature flag and requires setting up a custom Event filter/mapper +//! to capture logs. You also need to pass `enable_logs: true` in your `sentry::init` call. +//! +//! ``` +//! // assuming `tracing::Level::INFO => EventFilter::Log` in your `event_filter` +//! for i in 0..10 { +//! tracing::info!(number = i, my.key = "val", my.num = 42, "This is a log"); +//! } +//! ``` +//! +//! The fields of a `tracing` event are captured as attributes of the log. +//! Logs can be viewed and queried in the Logs explorer based on message and attributes. +//! Fields containing dots will be displayed as nested under their common prefix in the UI. +//! //! # Tracking Errors //! //! The easiest way to emit errors is by logging an event with `ERROR` level. This will create a @@ -103,10 +141,35 @@ //! tracing::error!(error = &custom_error as &dyn Error, "my operation failed"); //! ``` //! +//! # Sending multiple items to Sentry +//! +//! To map a `tracing` event to multiple items in Sentry, you can combine multiple event filters +//! using the bitwise or operator: +//! +//! ``` +//! use sentry::integrations::tracing::EventFilter; +//! use tracing_subscriber::prelude::*; +//! +//! let sentry_layer = sentry::integrations::tracing::layer() +//! .event_filter(|md| match *md.level() { +//! tracing::Level::ERROR => EventFilter::Event | EventFilter::Log, +//! tracing::Level::TRACE => EventFilter::Ignore, +//! _ => EventFilter::Log, +//! }) +//! .span_filter(|md| matches!(*md.level(), tracing::Level::ERROR | tracing::Level::WARN)); +//! +//! tracing_subscriber::registry() +//! .with(tracing_subscriber::fmt::layer()) +//! .with(sentry_layer) +//! .init(); +//! ``` +//! +//! If you're using a custom event mapper instead of an event filter, use `EventMapping::Combined`. +//! //! # Tracing Spans //! //! The integration automatically tracks `tracing` spans as spans in Sentry. A convenient way to do -//! this is with the `#[instrument]` attribute macro, which creates a transaction for the function +//! this is with the `#[instrument]` attribute macro, which creates a span/transaction for the function //! in Sentry. //! //! Function arguments are added as context fields automatically, which can be configured through @@ -117,8 +180,8 @@ //! //! use tracing_subscriber::prelude::*; //! -//! // Functions instrumented by tracing automatically report -//! // their span as transactions. +//! // Functions instrumented by tracing automatically +//! // create spans/transactions around their execution. //! #[tracing::instrument] //! async fn outer() { //! for i in 0..10 { @@ -135,6 +198,42 @@ //! tokio::time::sleep(Duration::from_millis(100)).await; //! } //! ``` +//! +//! By default, the name of the span sent to Sentry matches the name of the `tracing` span, which +//! is the name of the function when using `tracing::instrument`, or the name passed to the +//! `tracing::_span` macros. +//! +//! By default, the `op` of the span sent to Sentry is `default`. +//! +//! ## Special Span Fields +//! +//! Some fields on spans are treated specially by the Sentry tracing integration: +//! - `sentry.name`: overrides the span name sent to Sentry. +//! This is useful to customize the span name when using `#[tracing::instrument]`, or to update +//! it retroactively (using `span.record`) after the span has been created. +//! - `sentry.op`: overrides the span `op` sent to Sentry. +//! - `sentry.trace`: in Sentry, the `sentry-trace` header is sent with HTTP requests to achieve distributed tracing. +//! If the value of this field is set to the value of a valid `sentry-trace` header, which +//! other Sentry SDKs send automatically with outgoing requests, then the SDK will continue the trace using the given distributed tracing information. +//! This is useful to achieve distributed tracing at service boundaries by using only the +//! `tracing` API. +//! Note that `sentry.trace` will only be effective on span creation (it cannot be applied retroactively) +//! and requires the span it's applied to to be a root span, i.e. no span should active upon its +//! creation. +//! +//! +//! Example: +//! +//! ``` +//! #[tracing::instrument(skip_all, fields( +//! sentry.name = "GET /payments", +//! sentry.op = "http.server", +//! sentry.trace = headers.get("sentry-trace").unwrap_or(&"".to_owned()), +//! ))] +//! async fn handle_request(headers: std::collections::HashMap) { +//! // ... +//! } +//! ``` #![doc(html_favicon_url = "https://sentry-brand.storage.googleapis.com/favicon.ico")] #![doc(html_logo_url = "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png")] @@ -145,3 +244,8 @@ mod layer; pub use converters::*; pub use layer::*; + +const TAGS_PREFIX: &str = "tags."; +const SENTRY_OP_FIELD: &str = "sentry.op"; +const SENTRY_NAME_FIELD: &str = "sentry.name"; +const SENTRY_TRACE_FIELD: &str = "sentry.trace"; diff --git a/sentry-tracing/tests/README.md b/sentry-tracing/tests/README.md new file mode 100644 index 000000000..a5c57a9e2 --- /dev/null +++ b/sentry-tracing/tests/README.md @@ -0,0 +1,3 @@ +# Integration tests for `sentry-tracing` + +These tests are split up into 1 file per test to ensure they don't run concurrently. diff --git a/sentry-tracing/tests/breadcrumbs.rs b/sentry-tracing/tests/breadcrumbs.rs new file mode 100644 index 000000000..e163112db --- /dev/null +++ b/sentry-tracing/tests/breadcrumbs.rs @@ -0,0 +1,34 @@ +mod shared; + +#[test] +fn breadcrumbs_should_capture_span_fields() { + let transport = shared::init_sentry(0.0); // This test should work even if we are not sampling transactions. + + foo(); + + let data = transport.fetch_and_clear_envelopes(); + assert_eq!(data.len(), 1); + + let event = data.first().expect("should have 1 event"); + let event = match event.items().next().unwrap() { + sentry::protocol::EnvelopeItem::Event(event) => event, + unexpected => panic!("Expected event, but got {unexpected:#?}"), + }; + + assert_eq!(event.breadcrumbs.len(), 1); + assert_eq!( + event.breadcrumbs[0].data["foo:contextual_value"], + serde_json::Value::from(42) + ); + assert_eq!( + event.breadcrumbs[0].message, + Some("executing foo".to_owned()) + ); +} + +#[tracing::instrument(fields(contextual_value = 42))] +fn foo() { + tracing::info!("executing foo"); + + tracing::error!("boom!"); +} diff --git a/sentry-tracing/tests/name_op_updates.rs b/sentry-tracing/tests/name_op_updates.rs new file mode 100644 index 000000000..a6f3d15a9 --- /dev/null +++ b/sentry-tracing/tests/name_op_updates.rs @@ -0,0 +1,45 @@ +mod shared; + +#[tracing::instrument(fields( + some = "value", + sentry.name = "updated name", + sentry.op = "updated op", +))] +fn test_fun_record_on_creation() {} + +#[tracing::instrument(fields( + some = "value", + sentry.name = tracing::field::Empty, + sentry.op = tracing::field::Empty, +))] +fn test_fun_record_later() { + tracing::Span::current().record("sentry.name", "updated name"); + tracing::Span::current().record("sentry.op", "updated op"); +} + +#[test] +fn should_update_sentry_op_and_name_based_on_fields() { + let transport = shared::init_sentry(1.0); + + for f in [test_fun_record_on_creation, test_fun_record_later] { + f(); + + let data = transport.fetch_and_clear_envelopes(); + assert_eq!(data.len(), 1); + + let transaction = data.first().expect("should have 1 transaction"); + let transaction = match transaction.items().next().unwrap() { + sentry::protocol::EnvelopeItem::Transaction(transaction) => transaction, + unexpected => panic!("Expected transaction, but got {unexpected:#?}"), + }; + + assert_eq!(transaction.name.as_deref().unwrap(), "updated name"); + let ctx = transaction.contexts.get("trace"); + match ctx { + Some(sentry::protocol::Context::Trace(trace_ctx)) => { + assert_eq!(trace_ctx.op, Some("updated op".to_owned())) + } + _ => panic!("expected trace context"), + } + } +} diff --git a/sentry-tracing/tests/shared.rs b/sentry-tracing/tests/shared.rs new file mode 100644 index 000000000..4deadac5c --- /dev/null +++ b/sentry-tracing/tests/shared.rs @@ -0,0 +1,24 @@ +use sentry::{ClientOptions, Hub}; +use sentry_core::test::TestTransport; + +use std::sync::Arc; + +pub fn init_sentry(traces_sample_rate: f32) -> Arc { + use tracing_subscriber::prelude::*; + + let transport = TestTransport::new(); + let options = ClientOptions { + dsn: Some("https://test@sentry-tracing.com/test".parse().unwrap()), + transport: Some(Arc::new(transport.clone())), + sample_rate: 1.0, + traces_sample_rate, + ..ClientOptions::default() + }; + Hub::current().bind_client(Some(Arc::new(options.into()))); + + let _ = tracing_subscriber::registry() + .with(sentry_tracing::layer().enable_span_attributes()) + .try_init(); + + transport +} diff --git a/sentry-tracing/tests/smoke.rs b/sentry-tracing/tests/smoke.rs new file mode 100644 index 000000000..5778c662c --- /dev/null +++ b/sentry-tracing/tests/smoke.rs @@ -0,0 +1,65 @@ +mod shared; + +#[tracing::instrument(fields(tags.tag = "key", not_tag = "value"))] +fn function_with_tags(value: i32) { + tracing::error!(value, "event"); +} + +#[test] +fn should_instrument_function_with_event() { + let transport = shared::init_sentry(1.0); // Sample all spans. + + function_with_tags(1); + + let data = transport.fetch_and_clear_envelopes(); + assert_eq!(data.len(), 2); + let event = data.first().expect("should have 1 event"); + let event = match event.items().next().unwrap() { + sentry::protocol::EnvelopeItem::Event(event) => event, + unexpected => panic!("Expected event, but got {unexpected:#?}"), + }; + + //Validate transaction is created + let trace = match event.contexts.get("trace").expect("to get 'trace' context") { + sentry::protocol::Context::Trace(trace) => trace, + unexpected => panic!("Expected trace context but got {unexpected:?}"), + }; + assert_eq!(trace.op.as_deref().unwrap(), "smoke::function_with_tags"); + + //Confirm transaction values + let transaction = data.get(1).expect("should have 1 transaction"); + let transaction = match transaction.items().next().unwrap() { + sentry::protocol::EnvelopeItem::Transaction(transaction) => transaction, + unexpected => panic!("Expected transaction, but got {unexpected:#?}"), + }; + assert_eq!(transaction.name, Some("function_with_tags".into())); + assert_eq!(transaction.tags.len(), 1); + assert_eq!(trace.data.len(), 6); + + let tag = transaction + .tags + .get("tag") + .expect("to have tag with name 'tag'"); + assert_eq!(tag, "key"); + let not_tag = trace + .data + .get("not_tag") + .expect("to have data attribute with name 'not_tag'"); + assert_eq!(not_tag, "value"); + let value = trace + .data + .get("value") + .expect("to have data attribute with name 'value'"); + assert_eq!(value, 1); + + assert_eq!( + trace.data.get("sentry.tracing.target"), + Some("smoke".into()).as_ref() + ); + assert_eq!( + trace.data.get("code.module.name"), + Some("smoke".into()).as_ref() + ); + assert!(trace.data.contains_key("code.file.path")); + assert!(trace.data.contains_key("code.line.number")); +} diff --git a/sentry-types/Cargo.toml b/sentry-types/Cargo.toml index c2476a762..f65adb139 100644 --- a/sentry-types/Cargo.toml +++ b/sentry-types/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "sentry-types" -version = "0.32.2" +version = "0.45.0" authors = ["Sentry "] -license = "Apache-2.0" +license = "MIT" readme = "README.md" repository = "https://github.com/getsentry/sentry-rust" homepage = "https://sentry.io/welcome/" @@ -11,7 +11,7 @@ Common reusable types for implementing the sentry.io protocol. """ keywords = ["sentry", "protocol"] edition = "2021" -rust-version = "1.73" +rust-version = "1.81" [package.metadata.docs.rs] all-features = true @@ -19,18 +19,17 @@ all-features = true [features] default = ["protocol"] protocol = [] -UNSTABLE_metrics = [] [dependencies] debugid = { version = "0.8.0", features = ["serde"] } hex = "0.4.3" -rand = "0.8.5" +rand = "0.9.0" serde = { version = "1.0.104", features = ["derive"] } serde_json = "1.0.46" -thiserror = "1.0.15" +thiserror = "2.0.12" time = { version = "0.3.5", features = ["formatting", "parsing"] } url = { version = "2.1.1", features = ["serde"] } uuid = { version = "1.0.0", features = ["serde"] } [dev-dependencies] -rstest = "0.18.2" +rstest = "0.25.0" diff --git a/sentry-types/LICENSE b/sentry-types/LICENSE index d97d9399c..3e4988928 100644 --- a/sentry-types/LICENSE +++ b/sentry-types/LICENSE @@ -1,203 +1,21 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 Functional Software, Inc. dba Sentry (https://sentry.io) - and individual contributors. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2021 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sentry-types/README.md b/sentry-types/README.md index 556fd3b37..b9a61db2f 100644 --- a/sentry-types/README.md +++ b/sentry-types/README.md @@ -26,7 +26,7 @@ so later versions might be added later. ### API Concepts Most types are directly serializable or deserializable and try to implement -the `Default` type. This means that objects can be created conviently +the `Default` type. This means that objects can be created conveniently and missing attributes can be filled in: ```rust @@ -42,7 +42,7 @@ let event = v7::Event { ## Resources -License: Apache-2.0 +License: MIT - [Discord](https://discord.gg/ez5KZN7) server for project discussions. - Follow [@getsentry](https://twitter.com/getsentry) on Twitter for updates diff --git a/sentry-types/src/lib.rs b/sentry-types/src/lib.rs index 321e1088b..7f662ca9d 100644 --- a/sentry-types/src/lib.rs +++ b/sentry-types/src/lib.rs @@ -18,7 +18,7 @@ //! ## API Concepts //! //! Most types are directly serializable or deserializable and try to implement -//! the `Default` type. This means that objects can be created conviently +//! the `Default` type. This means that objects can be created conveniently //! and missing attributes can be filled in: //! //! ```rust diff --git a/sentry-types/src/protocol/attachment.rs b/sentry-types/src/protocol/attachment.rs index 8b2ce6d74..e23a64784 100644 --- a/sentry-types/src/protocol/attachment.rs +++ b/sentry-types/src/protocol/attachment.rs @@ -3,7 +3,7 @@ use std::fmt; use serde::Deserialize; /// The different types an attachment can have. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] pub enum AttachmentType { #[serde(rename = "event.attachment")] /// (default) A standard attachment without special meaning. @@ -23,6 +23,9 @@ pub enum AttachmentType { /// the last logs are extracted into event breadcrumbs. #[serde(rename = "unreal.logs")] UnrealLogs, + /// A custom attachment type with an arbitrary string value. + #[serde(untagged)] + Custom(String), } impl Default for AttachmentType { @@ -33,13 +36,14 @@ impl Default for AttachmentType { impl AttachmentType { /// Gets the string value Sentry expects for the attachment type. - pub fn as_str(self) -> &'static str { + pub fn as_str(&self) -> &str { match self { Self::Attachment => "event.attachment", Self::Minidump => "event.minidump", Self::AppleCrashReport => "event.applecrashreport", Self::UnrealContext => "unreal.context", Self::UnrealLogs => "unreal.logs", + Self::Custom(s) => s, } } } @@ -68,7 +72,11 @@ impl Attachment { r#"{{"type":"attachment","length":{length},"filename":"{filename}","attachment_type":"{at}","content_type":"{ct}"}}"#, filename = self.filename, length = self.buffer.len(), - at = self.ty.unwrap_or_default().as_str(), + at = self + .ty + .as_ref() + .unwrap_or(&AttachmentType::default()) + .as_str(), ct = self .content_type .as_ref() @@ -92,3 +100,18 @@ impl fmt::Debug for Attachment { .finish() } } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json; + + #[test] + fn test_attachment_type_deserialize() { + let result: AttachmentType = serde_json::from_str(r#""event.minidump""#).unwrap(); + assert_eq!(result, AttachmentType::Minidump); + + let result: AttachmentType = serde_json::from_str(r#""my.custom.type""#).unwrap(); + assert_eq!(result, AttachmentType::Custom("my.custom.type".to_string())); + } +} diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 56809a7f2..3a9bed50d 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -1,14 +1,17 @@ -use std::{io::Write, path::Path}; +use std::{io::Write, path::Path, time::SystemTime}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; +use crate::utils::ts_rfc3339_opt; +use crate::Dsn; + use super::v7 as protocol; use protocol::{ - Attachment, AttachmentType, Event, MonitorCheckIn, SessionAggregates, SessionUpdate, - Transaction, + Attachment, AttachmentType, ClientSdkInfo, DynamicSamplingContext, Event, Log, MonitorCheckIn, + SessionAggregates, SessionUpdate, Transaction, }; /// Raised if a envelope cannot be parsed from a given input. @@ -37,9 +40,66 @@ pub enum EnvelopeError { InvalidItemPayload(#[source] serde_json::Error), } -#[derive(Deserialize)] -struct EnvelopeHeader { +/// The supported [Sentry Envelope Headers](https://develop.sentry.dev/sdk/data-model/envelopes/#headers). +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct EnvelopeHeaders { + #[serde(default, skip_serializing_if = "Option::is_none")] event_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + dsn: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + sdk: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "ts_rfc3339_opt" + )] + sent_at: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + trace: Option, +} + +impl EnvelopeHeaders { + /// Creates empty Envelope headers. + pub fn new() -> EnvelopeHeaders { + Default::default() + } + + /// Sets the Event ID. + #[must_use] + pub fn with_event_id(mut self, event_id: Uuid) -> Self { + self.event_id = Some(event_id); + self + } + + /// Sets the DSN. + #[must_use] + pub fn with_dsn(mut self, dsn: Dsn) -> Self { + self.dsn = Some(dsn); + self + } + + /// Sets the SDK information. + #[must_use] + pub fn with_sdk(mut self, sdk: ClientSdkInfo) -> Self { + self.sdk = Some(sdk); + self + } + + /// Sets the time this envelope was sent at. + /// This timestamp should be generated as close as possible to the transmission of the event. + #[must_use] + pub fn with_sent_at(mut self, sent_at: SystemTime) -> Self { + self.sent_at = Some(sent_at); + self + } + + /// Sets the Dynamic Sampling Context. + #[must_use] + pub fn with_trace(mut self, trace: DynamicSamplingContext) -> Self { + self.trace = Some(trace); + self + } } /// An Envelope Item Type. @@ -61,13 +121,12 @@ enum EnvelopeItemType { /// An Attachment Item type. #[serde(rename = "attachment")] Attachment, - /// A Monitor Check In Item Type + /// A Monitor Check In Item Type. #[serde(rename = "check_in")] MonitorCheckIn, - /// A Metrics Item type. - #[cfg(feature = "UNSTABLE_metrics")] - #[serde(rename = "statsd")] - Metrics, + /// A container of Log items. + #[serde(rename = "log")] + LogsContainer, } /// An Envelope Item Header. @@ -75,10 +134,11 @@ enum EnvelopeItemType { struct EnvelopeItemHeader { r#type: EnvelopeItemType, length: Option, - // Fields below apply only to Attachment Item type + // Applies both to Attachment and ItemContainer Item type + content_type: Option, + // Fields below apply only to Attachment Item types filename: Option, attachment_type: Option, - content_type: Option, } /// An Envelope Item. @@ -116,15 +176,64 @@ pub enum EnvelopeItem { Attachment(Attachment), /// A MonitorCheckIn item. MonitorCheckIn(MonitorCheckIn), - /// A Metrics Item. - #[cfg(feature = "UNSTABLE_metrics")] - Statsd(Vec), + /// A container for a list of multiple items. + ItemContainer(ItemContainer), /// This is a sentinel item used to `filter` raw envelopes. Raw, // TODO: // etc… } +/// A container for a list of multiple items. +/// It's considered a single envelope item, with its `type` corresponding to the contained items' +/// `type`. +#[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] +pub enum ItemContainer { + /// A list of logs. + Logs(Vec), +} + +#[allow(clippy::len_without_is_empty, reason = "is_empty is not needed")] +impl ItemContainer { + /// The number of items in this item container. + pub fn len(&self) -> usize { + match self { + Self::Logs(logs) => logs.len(), + } + } + + /// The `type` of this item container, which corresponds to the `type` of the contained items. + pub fn ty(&self) -> &'static str { + match self { + Self::Logs(_) => "log", + } + } + + /// The `content-type` expected by Relay for this item container. + pub fn content_type(&self) -> &'static str { + match self { + Self::Logs(_) => "application/vnd.sentry.items.log+json", + } + } +} + +impl From> for ItemContainer { + fn from(logs: Vec) -> Self { + Self::Logs(logs) + } +} + +#[derive(Serialize)] +struct LogsSerializationWrapper<'a> { + items: &'a [Log], +} + +#[derive(Deserialize)] +struct LogsDeserializationWrapper { + items: Vec, +} + impl From> for EnvelopeItem { fn from(event: Event<'static>) -> Self { EnvelopeItem::Event(event) @@ -161,6 +270,18 @@ impl From for EnvelopeItem { } } +impl From for EnvelopeItem { + fn from(container: ItemContainer) -> Self { + EnvelopeItem::ItemContainer(container) + } +} + +impl From> for EnvelopeItem { + fn from(logs: Vec) -> Self { + EnvelopeItem::ItemContainer(logs.into()) + } +} + /// An Iterator over the items of an Envelope. #[derive(Clone)] pub struct EnvelopeItemIter<'s> { @@ -210,7 +331,7 @@ impl Items { /// for more details. #[derive(Clone, Default, Debug, PartialEq)] pub struct Envelope { - event_id: Option, + headers: EnvelopeHeaders, items: Items, } @@ -236,18 +357,18 @@ impl Envelope { return; }; - if self.event_id.is_none() { + if self.headers.event_id.is_none() { if let EnvelopeItem::Event(ref event) = item { - self.event_id = Some(event.event_id); + self.headers.event_id = Some(event.event_id); } else if let EnvelopeItem::Transaction(ref transaction) = item { - self.event_id = Some(transaction.event_id); + self.headers.event_id = Some(transaction.event_id); } } items.push(item); } /// Create an [`Iterator`] over all the [`EnvelopeItem`]s. - pub fn items(&self) -> EnvelopeItemIter { + pub fn items(&self) -> EnvelopeItemIter<'_> { let inner = match &self.items { Items::EnvelopeItems(items) => items.iter(), Items::Raw(_) => [].iter(), @@ -256,9 +377,21 @@ impl Envelope { EnvelopeItemIter { inner } } + /// Returns the Envelope headers. + pub fn headers(&self) -> &EnvelopeHeaders { + &self.headers + } + + /// Sets the Envelope headers. + #[must_use] + pub fn with_headers(mut self, headers: EnvelopeHeaders) -> Self { + self.headers = headers; + self + } + /// Returns the Envelopes Uuid, if any. pub fn uuid(&self) -> Option<&Uuid> { - self.event_id.as_ref() + self.headers.event_id.as_ref() } /// Returns the [`Event`] contained in this Envelope, if any. @@ -330,11 +463,8 @@ impl Envelope { }; // write the headers: - let event_id = self.uuid(); - match event_id { - Some(uuid) => writeln!(writer, r#"{{"event_id":"{uuid}"}}"#)?, - _ => writeln!(writer, "{{}}")?, - } + serde_json::to_writer(&mut writer, &self.headers)?; + writeln!(writer)?; let mut item_buf = Vec::new(); // write each item: @@ -359,8 +489,12 @@ impl Envelope { EnvelopeItem::MonitorCheckIn(check_in) => { serde_json::to_writer(&mut item_buf, check_in)? } - #[cfg(feature = "UNSTABLE_metrics")] - EnvelopeItem::Statsd(statsd) => item_buf.extend_from_slice(statsd), + EnvelopeItem::ItemContainer(container) => match container { + ItemContainer::Logs(logs) => { + let wrapper = LogsSerializationWrapper { items: logs }; + serde_json::to_writer(&mut item_buf, &wrapper)? + } + }, EnvelopeItem::Raw => { continue; } @@ -371,16 +505,26 @@ impl Envelope { EnvelopeItem::SessionAggregates(_) => "sessions", EnvelopeItem::Transaction(_) => "transaction", EnvelopeItem::MonitorCheckIn(_) => "check_in", - #[cfg(feature = "UNSTABLE_metrics")] - EnvelopeItem::Statsd(_) => "statsd", + EnvelopeItem::ItemContainer(container) => container.ty(), EnvelopeItem::Attachment(_) | EnvelopeItem::Raw => unreachable!(), }; - writeln!( - writer, - r#"{{"type":"{}","length":{}}}"#, - item_type, - item_buf.len() - )?; + + if let EnvelopeItem::ItemContainer(container) = item { + writeln!( + writer, + r#"{{"type":"{}","item_count":{},"content_type":"{}"}}"#, + item_type, + container.len(), + container.content_type() + )?; + } else { + writeln!( + writer, + r#"{{"type":"{}","length":{}}}"#, + item_type, + item_buf.len() + )?; + } writer.write_all(&item_buf)?; writeln!(writer)?; item_buf.clear(); @@ -391,11 +535,11 @@ impl Envelope { /// Creates a new Envelope from slice. pub fn from_slice(slice: &[u8]) -> Result { - let (header, offset) = Self::parse_header(slice)?; + let (headers, offset) = Self::parse_headers(slice)?; let items = Self::parse_items(slice, offset)?; let mut envelope = Envelope { - event_id: header.event_id, + headers, ..Default::default() }; @@ -409,8 +553,8 @@ impl Envelope { /// Creates a new raw Envelope from the given buffer. pub fn from_bytes_raw(bytes: Vec) -> Result { Ok(Self { - event_id: None, items: Items::Raw(bytes), + ..Default::default() }) } @@ -429,19 +573,19 @@ impl Envelope { Self::from_bytes_raw(bytes) } - fn parse_header(slice: &[u8]) -> Result<(EnvelopeHeader, usize), EnvelopeError> { - let mut stream = serde_json::Deserializer::from_slice(slice).into_iter(); + fn parse_headers(slice: &[u8]) -> Result<(EnvelopeHeaders, usize), EnvelopeError> { + let first_line = slice + .split(|b| *b == b'\n') + .next() + .ok_or(EnvelopeError::MissingHeader)?; - let header: EnvelopeHeader = match stream.next() { - None => return Err(EnvelopeError::MissingHeader), - Some(Err(error)) => return Err(EnvelopeError::InvalidHeader(error)), - Some(Ok(header)) => header, - }; + let headers: EnvelopeHeaders = + serde_json::from_slice(first_line).map_err(EnvelopeError::InvalidHeader)?; - // Each header is terminated by a UNIX newline. - Self::require_termination(slice, stream.byte_offset())?; + let offset = first_line.len(); + Self::require_termination(slice, offset)?; - Ok((header, stream.byte_offset() + 1)) + Ok((headers, offset + 1)) } fn parse_items(slice: &[u8], mut offset: usize) -> Result, EnvelopeError> { @@ -517,8 +661,10 @@ impl Envelope { EnvelopeItemType::MonitorCheckIn => { serde_json::from_slice(payload).map(EnvelopeItem::MonitorCheckIn) } - #[cfg(feature = "UNSTABLE_metrics")] - EnvelopeItemType::Metrics => Ok(EnvelopeItem::Statsd(payload.into())), + EnvelopeItemType::LogsContainer => { + serde_json::from_slice::(payload) + .map(|x| EnvelopeItem::ItemContainer(ItemContainer::Logs(x.items))) + } } .map_err(EnvelopeError::InvalidItemPayload)?; @@ -533,26 +679,13 @@ impl Envelope { } } -impl From> for Envelope { - fn from(event: Event<'static>) -> Self { - let mut envelope = Self::default(); - envelope.add_item(event); - envelope - } -} - -impl From> for Envelope { - fn from(transaction: Transaction<'static>) -> Self { +impl From for Envelope +where + T: Into, +{ + fn from(item: T) -> Self { let mut envelope = Self::default(); - envelope.add_item(transaction); - envelope - } -} - -impl From for Envelope { - fn from(check_in: MonitorCheckIn) -> Self { - let mut envelope = Self::default(); - envelope.add_item(check_in); + envelope.add_item(item.into()); envelope } } @@ -562,12 +695,13 @@ mod test { use std::str::FromStr; use std::time::{Duration, SystemTime}; + use protocol::Map; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; use super::*; use crate::protocol::v7::{ - Level, MonitorCheckInStatus, MonitorConfig, MonitorSchedule, SessionAttributes, + Level, MonitorCheckInStatus, MonitorConfig, MonitorSchedule, SampleRand, SessionAttributes, SessionStatus, Span, }; @@ -783,7 +917,7 @@ some content let envelope = Envelope::from_slice(bytes).unwrap(); let event_id = Uuid::from_str("9ec79c33ec9942ab8353589fcb2e04dc").unwrap(); - assert_eq!(envelope.event_id, Some(event_id)); + assert_eq!(envelope.headers.event_id, Some(event_id)); assert_eq!(envelope.items().count(), 0); } @@ -946,6 +1080,35 @@ some content } } + #[test] + fn test_all_envelope_headers_roundtrip() { + let bytes = br#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c","sdk":{"name":"3e934135-3f2b-49bc-8756-9f025b55143e","version":"3e31738e-4106-42d0-8be2-4a3a1bc648d3","integrations":["daec50ae-8729-49b5-82f7-991446745cd5","8fc94968-3499-4a2c-b4d7-ecc058d9c1b0"],"packages":[{"name":"b59a1949-9950-4203-b394-ddd8d02c9633","version":"3d7790f3-7f32-43f7-b82f-9f5bc85205a8"}]},"sent_at":"2020-02-07T14:16:00Z","trace":{"trace_id":"65bcd18546c942069ed957b15b4ace7c","public_key":"5d593cac-f833-4845-bb23-4eabdf720da2","sample_rate":"0.00000021","sample_rand":"0.123456","sampled":"true","environment":"0666ab02-6364-4135-aa59-02e8128ce052","transaction":"0252ec25-cd0a-4230-bd2f-936a4585637e"}} +{"type":"event","length":74} +{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296} +"#; + + let envelope = Envelope::from_slice(bytes); + assert!(envelope.is_ok()); + let envelope = envelope.unwrap(); + let serialized = to_str(envelope); + assert_eq!(bytes, serialized.as_bytes()); + } + + #[test] + fn test_sample_rand_rounding() { + let envelope = Envelope::new().with_headers( + EnvelopeHeaders::new().with_trace( + DynamicSamplingContext::new() + .with_sample_rand(SampleRand::try_from(0.999_999_9).unwrap()), + ), + ); + let expected = br#"{"trace":{"sample_rand":"0.999999"}} +"#; + + let serialized = to_str(envelope); + assert_eq!(expected, serialized.as_bytes()); + } + // Test all possible item types in a single envelope #[test] fn test_deserialize_serialized() { @@ -995,12 +1158,39 @@ some content ..Default::default() }; - let mut envelope: Envelope = Envelope::new(); + let mut attributes = Map::new(); + attributes.insert("key".into(), "value".into()); + attributes.insert("num".into(), 10.into()); + attributes.insert("val".into(), 10.2.into()); + attributes.insert("bool".into(), false.into()); + let mut attributes_2 = attributes.clone(); + attributes_2.insert("more".into(), true.into()); + let logs: EnvelopeItem = vec![ + Log { + level: protocol::LogLevel::Warn, + body: "test".to_owned(), + trace_id: Some("335e53d614474acc9f89e632b776cc28".parse().unwrap()), + timestamp: timestamp("2022-07-25T14:51:14.296Z"), + severity_number: Some(1.try_into().unwrap()), + attributes, + }, + Log { + level: protocol::LogLevel::Error, + body: "a body".to_owned(), + trace_id: Some("332253d614472a2c9f89e232b7762c28".parse().unwrap()), + timestamp: timestamp("2021-07-21T14:51:14.296Z"), + severity_number: Some(1.try_into().unwrap()), + attributes: attributes_2, + }, + ] + .into(); + let mut envelope: Envelope = Envelope::new(); envelope.add_item(event); envelope.add_item(transaction); envelope.add_item(session); envelope.add_item(attachment); + envelope.add_item(logs); let serialized = to_str(envelope); let deserialized = Envelope::from_slice(serialized.as_bytes()).unwrap(); diff --git a/sentry-types/src/protocol/session.rs b/sentry-types/src/protocol/session.rs index f0feffff5..43ea9fc3e 100644 --- a/sentry-types/src/protocol/session.rs +++ b/sentry-types/src/protocol/session.rs @@ -127,7 +127,7 @@ pub struct SessionUpdate<'a> { #[serde(default)] pub status: SessionStatus, - /// The number of errors that ocurred. + /// The number of errors that occurred. #[serde(default)] pub errors: u64, @@ -150,16 +150,16 @@ pub struct SessionAggregateItem { /// The distinct identifier. #[serde(rename = "did", default, skip_serializing_if = "Option::is_none")] pub distinct_id: Option, - /// The number of exited sessions that ocurred. + /// The number of exited sessions that occurred. #[serde(default, skip_serializing_if = "is_zero")] pub exited: u32, - /// The number of errored sessions that ocurred, not including the abnormal and crashed ones. + /// The number of errored sessions that occurred, not including the abnormal and crashed ones. #[serde(default, skip_serializing_if = "is_zero")] pub errored: u32, - /// The number of abnormal sessions that ocurred. + /// The number of abnormal sessions that occurred. #[serde(default, skip_serializing_if = "is_zero")] pub abnormal: u32, - /// The number of crashed sessions that ocurred. + /// The number of crashed sessions that occurred. #[serde(default, skip_serializing_if = "is_zero")] pub crashed: u32, } diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index df1805a94..b2f8ee99a 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -18,10 +18,11 @@ use std::time::SystemTime; use self::debugid::{CodeId, DebugId}; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; -use url::Url; -use uuid::Uuid; -use crate::utils::{ts_rfc3339_opt, ts_seconds_float}; +pub use url::Url; +pub use uuid::Uuid; + +use crate::utils::{display_from_str_opt, ts_rfc3339_opt, ts_seconds_float}; pub use super::attachment::*; pub use super::envelope::*; @@ -781,7 +782,7 @@ impl Default for Breadcrumb { /// An IP address, either IPv4, IPv6 or Auto. #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Default)] pub enum IpAddress { - /// The IP address needs to be infered from the user's context. + /// The IP address needs to be inferred from the user's context. #[default] Auto, /// The exact given IP address (v4 or v6). @@ -1095,8 +1096,12 @@ pub enum Context { Browser(Box), /// Tracing data. Trace(Box), - /// GPU data + /// GPU data. Gpu(Box), + /// OpenTelemetry data. + Otel(Box), + /// HTTP response data. + Response(Box), /// Generic other context data. #[serde(rename = "unknown")] Other(Map), @@ -1113,6 +1118,8 @@ impl Context { Context::Browser(..) => "browser", Context::Trace(..) => "trace", Context::Gpu(..) => "gpu", + Context::Otel(..) => "otel", + Context::Response(..) => "response", Context::Other(..) => "unknown", } } @@ -1331,8 +1338,47 @@ pub struct GpuContext { pub other: Map, } +/// OpenTelemetry context +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] +pub struct OtelContext { + /// OpenTelemetry [general + /// attributes](https://opentelemetry.io/docs/specs/semconv/general/attributes/). + #[serde(default, skip_serializing_if = "Map::is_empty")] + pub attributes: Map, + /// OpenTelemetry [resource attributes](https://opentelemetry.io/docs/specs/semconv/resource/), + /// describing the entity producing telemetry. + #[serde(default, skip_serializing_if = "Map::is_empty")] + pub resource: Map, + /// Additional arbitrary fields for forwards compatibility. + #[serde(flatten)] + pub other: Map, +} + +/// Holds information about an HTTP response. +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] +pub struct ResponseContext { + /// The unparsed cookie values. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cookies: Option, + /// A map of submitted headers. + /// + /// If a header appears multiple times, it needs to be merged according to the HTTP standard + /// for header merging. Header names are treated case-insensitively by Sentry. + #[serde(default, skip_serializing_if = "Map::is_empty")] + pub headers: Map, + /// The HTTP response status code. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status_code: Option, + /// The response body size in bytes. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub body_size: Option, + /// Response data in any format that makes sense. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub data: Option, +} + /// Holds the identifier for a Span -#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash)] #[serde(try_from = "String", into = "String")] pub struct SpanId([u8; 8]); @@ -1348,6 +1394,12 @@ impl fmt::Display for SpanId { } } +impl fmt::Debug for SpanId { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "SpanId({self})") + } +} + impl From for String { fn from(span_id: SpanId) -> Self { span_id.to_string() @@ -1372,8 +1424,14 @@ impl TryFrom for SpanId { } } +impl From<[u8; 8]> for SpanId { + fn from(value: [u8; 8]) -> Self { + Self(value) + } +} + /// Holds the identifier for a Trace -#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash)] #[serde(try_from = "String", into = "String")] pub struct TraceId([u8; 16]); @@ -1389,6 +1447,12 @@ impl fmt::Display for TraceId { } } +impl fmt::Debug for TraceId { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "TraceId({self})") + } +} + impl From for String { fn from(trace_id: TraceId) -> Self { trace_id.to_string() @@ -1413,6 +1477,12 @@ impl TryFrom for TraceId { } } +impl From<[u8; 16]> for TraceId { + fn from(value: [u8; 16]) -> Self { + Self(value) + } +} + /// Holds information about a tracing event. #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] pub struct TraceContext { @@ -1434,6 +1504,13 @@ pub struct TraceContext { /// Describes the status of the span (e.g. `ok`, `cancelled`, etc.) #[serde(default, skip_serializing_if = "Option::is_none")] pub status: Option, + /// Describes what created the transaction. See the [develop + /// docs](https://develop.sentry.dev/sdk/telemetry/traces/trace-origin/) for more information. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub origin: Option, + /// Optional data attributes to be associated with the transaction. + #[serde(default, skip_serializing_if = "Map::is_empty")] + pub data: Map, } macro_rules! into_context { @@ -1453,8 +1530,12 @@ into_context!(Runtime, RuntimeContext); into_context!(Browser, BrowserContext); into_context!(Trace, TraceContext); into_context!(Gpu, GpuContext); +into_context!(Otel, OtelContext); +into_context!(Response, ResponseContext); -const INFERABLE_CONTEXTS: &[&str] = &["device", "os", "runtime", "app", "browser", "trace", "gpu"]; +const INFERABLE_CONTEXTS: &[&str] = &[ + "device", "os", "runtime", "app", "browser", "trace", "gpu", "otel", "response", +]; struct ContextsVisitor; @@ -1594,7 +1675,7 @@ pub struct Event<'a> { /// A release identifier. #[serde(default, skip_serializing_if = "Option::is_none")] pub release: Option>, - /// An optional distribution identifer. + /// An optional distribution identifier. #[serde(default, skip_serializing_if = "Option::is_none")] pub dist: Option>, /// An optional environment identifier. @@ -1642,7 +1723,7 @@ pub struct Event<'a> { pub sdk: Option>, } -impl<'a> Default for Event<'a> { +impl Default for Event<'_> { fn default() -> Self { Event { event_id: event::default_id(), @@ -1721,7 +1802,7 @@ impl<'a> Event<'a> { } } -impl<'a> fmt::Display for Event<'a> { +impl fmt::Display for Event<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, @@ -1799,6 +1880,11 @@ impl Span { Default::default() } + /// Finalizes the span with the provided timestamp. + pub fn finish_with_timestamp(&mut self, timestamp: SystemTime) { + self.timestamp = Some(timestamp); + } + /// Finalizes the span. pub fn finish(&mut self) { self.timestamp = Some(SystemTime::now()); @@ -1988,9 +2074,12 @@ pub struct Transaction<'a> { /// Optionally HTTP request data to be sent along. #[serde(default, skip_serializing_if = "Option::is_none")] pub request: Option, + /// Optionally the server (or device) name of this event. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub server_name: Option>, } -impl<'a> Default for Transaction<'a> { +impl Default for Transaction<'_> { fn default() -> Self { Transaction { event_id: event::default_id(), @@ -2007,6 +2096,7 @@ impl<'a> Default for Transaction<'a> { spans: Default::default(), contexts: Default::default(), request: Default::default(), + server_name: Default::default(), } } } @@ -2034,6 +2124,7 @@ impl<'a> Transaction<'a> { spans: self.spans, contexts: self.contexts, request: self.request, + server_name: self.server_name.map(|x| Cow::Owned(x.into_owned())), } } @@ -2041,9 +2132,14 @@ impl<'a> Transaction<'a> { pub fn finish(&mut self) { self.timestamp = Some(SystemTime::now()); } + + /// Finalizes the transaction to be dispatched with the given end timestamp. + pub fn finish_with_timestamp(&mut self, timestamp: SystemTime) { + self.timestamp = Some(timestamp); + } } -impl<'a> fmt::Display for Transaction<'a> { +impl fmt::Display for Transaction<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, @@ -2053,3 +2149,413 @@ impl<'a> fmt::Display for Transaction<'a> { ) } } + +/// A single [structured log](https://docs.sentry.io/product/explore/logs/). +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Log { + /// The severity of the log (required). + pub level: LogLevel, + /// The log body/message (required). + pub body: String, + /// The ID of the Trace in which this log happened (required). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub trace_id: Option, + /// The timestamp of the log (required). + #[serde(with = "ts_seconds_float")] + pub timestamp: SystemTime, + /// The severity number of the log. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub severity_number: Option, + /// Additional arbitrary attributes attached to the log. + #[serde(default, skip_serializing_if = "Map::is_empty")] + pub attributes: Map, +} + +/// Indicates the severity of a log, according to the +/// OpenTelemetry [`SeverityText`](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitytext) spec. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum LogLevel { + /// A fine-grained debugging event. + Trace, + /// A debugging event. + Debug, + /// An informational event. Indicates that an event happened. + Info, + /// A warning event. Not an error but is likely more important than an informational event. + Warn, + /// An error event. Something went wrong. + Error, + /// A fatal error such as application or system crash. + Fatal, +} + +/// A number indicating the severity of a log, according to the OpenTelemetry +/// [`SeverityNumber`](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber) spec. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)] +pub struct LogSeverityNumber(u8); + +impl LogSeverityNumber { + /// The minimum severity number. + pub const MIN: u8 = 1; + /// The maximum severity number. + pub const MAX: u8 = 24; +} + +impl TryFrom for LogSeverityNumber { + type Error = String; + + fn try_from(value: u8) -> Result { + if (LogSeverityNumber::MIN..=LogSeverityNumber::MAX).contains(&value) { + Ok(Self(value)) + } else { + Err(format!( + "Log severity number must be between {} and {}", + LogSeverityNumber::MIN, + LogSeverityNumber::MAX + )) + } + } +} + +/// An attribute that can be attached to a log. +#[derive(Clone, Debug, PartialEq)] +pub struct LogAttribute(pub Value); + +impl From for LogAttribute +where + Value: From, +{ + fn from(value: T) -> Self { + Self(Value::from(value)) + } +} + +impl Serialize for LogAttribute { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut state = serializer.serialize_struct("LogAttribute", 2)?; + + match &self.0 { + Value::String(s) => { + state.serialize_field("value", s.as_str())?; + state.serialize_field("type", "string")?; + } + Value::Number(n) => { + if let Some(i) = n.as_i64() { + state.serialize_field("value", &i)?; + state.serialize_field("type", "integer")?; + } else if let Some(u) = n.as_u64() { + // Converting to a f64 could lead to precision loss + state.serialize_field("value", &u.to_string())?; + state.serialize_field("type", "string")?; + } else if let Some(f) = n.as_f64() { + state.serialize_field("value", &f)?; + state.serialize_field("type", "double")?; + } else { + // This should be unreachable, as a `Value::Number` can only be built from an i64, u64 or f64 + state.serialize_field("value", &n.to_string())?; + state.serialize_field("type", "string")?; + } + } + Value::Bool(b) => { + state.serialize_field("value", &b)?; + state.serialize_field("type", "boolean")?; + } + // For any other type (Null, Array, Object), convert to string with JSON representation + _ => { + state.serialize_field("value", &self.0.to_string())?; + state.serialize_field("type", "string")?; + } + } + + state.end() + } +} + +impl<'de> Deserialize<'de> for LogAttribute { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::{self, MapAccess, Visitor}; + use std::fmt; + + struct LogAttributeVisitor; + + impl<'de> Visitor<'de> for LogAttributeVisitor { + type Value = LogAttribute; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a LogAttribute with value and type fields") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut value: Option = None; + let mut type_str: Option = None; + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "value" => { + if value.is_some() { + return Err(de::Error::duplicate_field("value")); + } + value = Some(map.next_value()?); + } + "type" => { + if type_str.is_some() { + return Err(de::Error::duplicate_field("type")); + } + type_str = Some(map.next_value()?); + } + _ => { + // Ignore unknown fields + let _: serde_json::Value = map.next_value()?; + } + } + } + + let value = value.ok_or_else(|| de::Error::missing_field("value"))?; + let type_str = type_str.ok_or_else(|| de::Error::missing_field("type"))?; + + match type_str.as_str() { + "string" => { + if !value.is_string() { + return Err(de::Error::custom( + "type is 'string' but value is not a string", + )); + } + } + "integer" => { + if !value.is_i64() { + return Err(de::Error::custom( + "type is 'integer' but value is not an integer", + )); + } + } + "double" => { + if !value.is_f64() { + return Err(de::Error::custom( + "type is 'double' but value is not a double", + )); + } + } + "boolean" => { + if !value.is_boolean() { + return Err(de::Error::custom( + "type is 'boolean' but value is not a boolean", + )); + } + } + _ => { + return Err(de::Error::custom(format!( + "expected type to be 'string' | 'integer' | 'double' | 'boolean', found {type_str}" + ))) + } + } + + Ok(LogAttribute(value)) + } + } + + deserializer.deserialize_map(LogAttributeVisitor) + } +} + +/// An ID that identifies an organization in the Sentry backend. +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] +pub struct OrganizationId(u64); + +impl From for OrganizationId { + fn from(value: u64) -> Self { + Self(value) + } +} + +impl std::str::FromStr for OrganizationId { + type Err = std::num::ParseIntError; + + fn from_str(s: &str) -> Result { + s.parse().map(Self) + } +} + +impl std::fmt::Display for OrganizationId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// A random number generated at the start of a trace by the head of trace SDK. +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] +pub struct SampleRand(f64); + +/// An error that indicates failure to construct a SampleRand. +#[derive(Debug, Error)] +pub enum InvalidSampleRandError { + /// Indicates that the given value cannot be converted to a f64 succesfully. + #[error("failed to parse f64: {0}")] + InvalidFloat(#[from] std::num::ParseFloatError), + + /// Indicates that the given float is outside of the valid range for a sample rand, that is the + /// half-open interval [0.0, 1.0). + #[error("sample rand value out of admissible interval [0.0, 1.0)")] + OutOfRange, +} + +impl TryFrom for SampleRand { + type Error = InvalidSampleRandError; + + fn try_from(value: f64) -> Result { + if !(0.0..1.0).contains(&value) { + return Err(InvalidSampleRandError::OutOfRange); + } + Ok(Self(value)) + } +} + +impl std::str::FromStr for SampleRand { + type Err = InvalidSampleRandError; + + fn from_str(s: &str) -> Result { + let x: f64 = s.parse().map_err(InvalidSampleRandError::InvalidFloat)?; + Self::try_from(x) + } +} + +impl std::fmt::Display for SampleRand { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Special case: "{:.6}" would round values greater than or equal to 0.9999995 to 1.0, + // as Rust uses [rounding half-to-even](https://doc.rust-lang.org/std/fmt/#precision). + // Round to 0.999999 instead to comply with spec. + if self.0 >= 0.9999995 { + write!(f, "0.999999") + } else { + write!(f, "{:.6}", self.0) + } + } +} + +/// The [Dynamic Sampling +/// Context](https://develop.sentry.dev/sdk/telemetry/traces/dynamic-sampling-context/). +/// +/// Sentry supports sampling at the server level through [Dynamic Sampling](https://docs.sentry.io/organization/dynamic-sampling/). +/// This feature allows users to specify target sample rates for each project via the frontend instead of requiring an application redeployment. +/// The backend needs additional information from the SDK to support these features, contained in +/// the Dynamic Sampling Context. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct DynamicSamplingContext { + // Strictly required fields + // Still typed as optional, as when deserializing an envelope created by an older SDK they might still be missing + #[serde(default, skip_serializing_if = "Option::is_none")] + trace_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + public_key: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "display_from_str_opt" + )] + sample_rate: Option, + // Required fields + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "display_from_str_opt" + )] + sample_rand: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "display_from_str_opt" + )] + sampled: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + release: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + environment: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + transaction: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "display_from_str_opt" + )] + org_id: Option, +} + +impl DynamicSamplingContext { + /// Creates an empty Dynamic Sampling Context. + pub fn new() -> Self { + Default::default() + } + + /// Sets the trace ID. + #[must_use] + pub fn with_trace_id(mut self, trace_id: TraceId) -> Self { + self.trace_id = Some(trace_id); + self + } + + /// Sets the DSN public key. + #[must_use] + pub fn with_public_key(mut self, public_key: String) -> Self { + self.public_key = Some(public_key); + self + } + + /// Sets the sample rate. + #[must_use] + pub fn with_sample_rate(mut self, sample_rate: f32) -> Self { + self.sample_rate = Some(sample_rate); + self + } + + /// Sets the sample random value generated by the head of trace SDK. + #[must_use] + pub fn with_sample_rand(mut self, sample_rand: SampleRand) -> Self { + self.sample_rand = Some(sample_rand); + self + } + + /// Sets the sampled flag. + #[must_use] + pub fn with_sampled(mut self, sampled: bool) -> Self { + self.sampled = Some(sampled); + self + } + + /// Sets the release. + #[must_use] + pub fn with_release(mut self, release: String) -> Self { + self.release = Some(release); + self + } + + /// Sets the environment. + #[must_use] + pub fn with_environment(mut self, environment: String) -> Self { + self.environment = Some(environment); + self + } + + /// Sets the transaction. + #[must_use] + pub fn with_transaction(mut self, transaction: String) -> Self { + self.transaction = Some(transaction); + self + } + + /// Sets the organization ID. + #[must_use] + pub fn with_org_id(mut self, org_id: OrganizationId) -> Self { + self.org_id = Some(org_id); + self + } +} diff --git a/sentry-types/src/utils.rs b/sentry-types/src/utils.rs index aa3cd2a05..83d7a6581 100644 --- a/sentry-types/src/utils.rs +++ b/sentry-types/src/utils.rs @@ -60,7 +60,7 @@ pub mod ts_seconds_float { struct SecondsTimestampVisitor; - impl<'de> de::Visitor<'de> for SecondsTimestampVisitor { + impl de::Visitor<'_> for SecondsTimestampVisitor { type Value = SystemTime; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { @@ -144,7 +144,7 @@ pub mod ts_rfc3339 { pub(super) struct Rfc3339Deserializer; - impl<'de> de::Visitor<'de> for Rfc3339Deserializer { + impl de::Visitor<'_> for Rfc3339Deserializer { type Value = SystemTime; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { @@ -189,6 +189,65 @@ pub mod ts_rfc3339_opt { } } +/// Serialize and deserialize the inner value into/from a string using the `ToString`/`FromStr` implementation. +/// +/// # Example +/// +/// ```ignore +/// use serde::{Deserialize, Serialize}; +/// +/// #[derive(Debug, PartialEq, Serialize, Deserialize)] +/// struct Config { +/// #[serde(with = "sentry_types::utils::display_from_str_opt")] +/// host: Option, +/// #[serde(with = "sentry_types::utils::display_from_str_opt")] +/// port: Option, +/// #[serde(with = "sentry_types::utils::display_from_str_opt")] +/// enabled: Option, +/// } +/// +/// let config = Config { +/// host: Some("localhost".to_string()), +/// port: Some(8080), +/// enabled: Some(true), +/// }; +/// let json = serde_json::to_string(&config).unwrap(); +/// assert_eq!(json, r#"{"host":"localhost","port":"8080","enabled":"true"}"#); +/// +/// let deserialized: Config = serde_json::from_str(&json).unwrap(); +/// assert_eq!(deserialized, config); +/// ``` +pub(crate) mod display_from_str_opt { + use serde::{de, ser, Deserialize}; + + pub fn serialize(value: &Option, serializer: S) -> Result + where + T: ToString, + S: ser::Serializer, + { + match value { + Some(t) => serializer.serialize_str(&t.to_string()), + None => serializer.serialize_none(), + } + } + + pub fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> + where + T: std::str::FromStr, + T::Err: std::fmt::Display, + D: de::Deserializer<'de>, + { + let opt_string = Option::::deserialize(deserializer)?; + + match opt_string { + Some(s) => T::from_str(&s) + .map(Some) + .map_err(|e| de::Error::custom(format!("failed to parse string to type: {e}"))), + None => Ok(None), + } + } +} + #[cfg(test)] mod tests { use super::timestamp_to_datetime; diff --git a/sentry-types/tests/test_protocol_v7.rs b/sentry-types/tests/test_protocol_v7.rs index ea5b92756..841cf19d1 100644 --- a/sentry-types/tests/test_protocol_v7.rs +++ b/sentry-types/tests/test_protocol_v7.rs @@ -1313,6 +1313,43 @@ mod test_contexts { ); } + #[test] + fn test_response_context() { + let event = v7::Event { + event_id: event_id(), + timestamp: event_time(), + contexts: { + let mut m = v7::Map::new(); + m.insert( + "response".into(), + v7::ResponseContext { + status_code: Some(400), + cookies: Some("sessionId=abc123; Path=/; HttpOnly,authToken=xyz789; Secure; SameSite=Strict".into()), + headers: { + let mut hm = v7::Map::new(); + hm.insert("Content-Type".into(), "text/plain".into()); + hm + }, + body_size: Some(1000), + data: Some("lol".into()), + } + .into(), + ); + m + }, + ..Default::default() + }; + + assert_roundtrip(&event); + assert_eq!( + serde_json::to_string(&event).unwrap(), + "{\"event_id\":\"d43e86c96e424a93a4fbda156dd17341\",\"timestamp\":1514103120,\ + \"contexts\":{\"response\":{\"type\":\"response\",\ + \"cookies\":\"sessionId=abc123; Path=/; HttpOnly,authToken=xyz789; Secure; SameSite=Strict\",\ + \"headers\":{\"Content-Type\":\"text/plain\"},\"status_code\":400,\"body_size\":1000,\"data\":\"lol\"}}}" + ); + } + #[test] fn test_renamed_contexts() { let event = v7::Event { @@ -1523,3 +1560,48 @@ fn test_orientation() { "\"portrait\"" ); } + +mod test_logs { + use sentry_types::protocol::v7::LogAttribute; + use serde_json::{json, Value}; + + #[test] + fn test_log_attribute_serialization() { + let attributes: Vec<(LogAttribute, &str)> = vec![ + // Supported types + (42.into(), r#"{"value":42,"type":"integer"}"#), + (3.1.into(), r#"{"value":3.1,"type":"double"}"#), + ("lol".into(), r#"{"value":"lol","type":"string"}"#), + (false.into(), r#"{"value":false,"type":"boolean"}"#), + // Special cases + (Value::Null.into(), r#"{"value":"null","type":"string"}"#), + ( + (u64::MAX - 1).into(), + r#"{"value":"18446744073709551614","type":"string"}"#, + ), + // Unsupported types (for now) + ( + json!(r#"[1,2,3,4]"#).into(), + r#"{"value":"[1,2,3,4]","type":"string"}"#, + ), + ( + json!(r#"["a","b","c"]"#).into(), + r#"{"value":"[\"a\",\"b\",\"c\"]","type":"string"}"#, + ), + ]; + for (attribute, expected) in attributes { + let actual = serde_json::to_string(&attribute).unwrap(); + assert_eq!(expected, actual); + } + } + + #[test] + fn test_log_attribute_roundtrip() { + let attributes: Vec = vec![42.into(), 3.1.into(), "lol".into(), false.into()]; + for expected in attributes { + let serialized = serde_json::to_string(&expected).unwrap(); + let actual: LogAttribute = serde_json::from_str(&serialized).unwrap(); + assert_eq!(expected, actual); + } + } +} diff --git a/sentry/Cargo.toml b/sentry/Cargo.toml index a9ff2cecb..c44a55a7d 100644 --- a/sentry/Cargo.toml +++ b/sentry/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "sentry" -version = "0.32.2" +version = "0.45.0" authors = ["Sentry "] -license = "Apache-2.0" +license = "MIT" readme = "README.md" repository = "https://github.com/getsentry/sentry-rust" homepage = "https://sentry.io/welcome/" @@ -10,7 +10,7 @@ description = """ Sentry (getsentry.com) client for rust ;) """ edition = "2021" -rust-version = "1.73" +rust-version = "1.81" autoexamples = true # To build locally: @@ -21,8 +21,14 @@ all-features = true rustdoc-args = ["--cfg", "doc_cfg"] [features] -default = ["backtrace", "contexts", "debug-images", "panic", "transport"] -UNSTABLE_metrics = ["sentry-core/UNSTABLE_metrics"] +default = [ + "backtrace", + "contexts", + "debug-images", + "panic", + "transport", + "release-health", +] # default integrations backtrace = ["sentry-backtrace", "sentry-tracing?/backtrace"] @@ -30,6 +36,7 @@ contexts = ["sentry-contexts"] panic = ["sentry-panic"] # other integrations anyhow = ["sentry-anyhow"] +actix = ["sentry-actix"] debug-images = ["sentry-debug-images"] log = ["sentry-log"] slog = ["sentry-slog"] @@ -37,51 +44,50 @@ tower = ["sentry-tower"] tower-http = ["tower", "sentry-tower/http"] tower-axum-matched-path = ["tower-http", "sentry-tower/axum-matched-path"] tracing = ["sentry-tracing"] +opentelemetry = ["sentry-opentelemetry"] # other features test = ["sentry-core/test"] -debug-logs = ["dep:log", "sentry-core/debug-logs"] +release-health = ["sentry-core/release-health", "sentry-actix?/release-health"] +logs = ["sentry-core/logs", "sentry-tracing?/logs", "sentry-log?/logs"] # transports transport = ["reqwest", "native-tls"] reqwest = ["dep:reqwest", "httpdate", "tokio"] curl = ["dep:curl", "httpdate"] -surf-h1 = ["surf/h1-client", "httpdate"] -surf = ["surf/curl-client", "http-client", "httpdate", "isahc", "tokio"] ureq = ["dep:ureq", "httpdate"] # transport settings native-tls = ["dep:native-tls", "reqwest?/default-tls", "ureq?/native-tls"] -rustls = ["dep:rustls", "reqwest?/rustls-tls", "ureq?/tls", "webpki-roots"] +rustls = ["dep:rustls", "reqwest?/rustls-tls", "ureq?/rustls"] +embedded-svc-http = ["dep:embedded-svc", "dep:esp-idf-svc"] [dependencies] -sentry-core = { version = "0.32.2", path = "../sentry-core", features = [ +sentry-core = { version = "0.45.0", path = "../sentry-core", features = [ "client", ] } -sentry-anyhow = { version = "0.32.2", path = "../sentry-anyhow", optional = true } -sentry-backtrace = { version = "0.32.2", path = "../sentry-backtrace", optional = true } -sentry-contexts = { version = "0.32.2", path = "../sentry-contexts", optional = true } -sentry-debug-images = { version = "0.32.2", path = "../sentry-debug-images", optional = true } -sentry-log = { version = "0.32.2", path = "../sentry-log", optional = true } -sentry-panic = { version = "0.32.2", path = "../sentry-panic", optional = true } -sentry-slog = { version = "0.32.2", path = "../sentry-slog", optional = true } -sentry-tower = { version = "0.32.2", path = "../sentry-tower", optional = true } -sentry-tracing = { version = "0.32.2", path = "../sentry-tracing", optional = true } -log = { version = "0.4.8", optional = true, features = ["std"] } +sentry-anyhow = { version = "0.45.0", path = "../sentry-anyhow", optional = true } +sentry-actix = { version = "0.45.0", path = "../sentry-actix", optional = true, default-features = false } +sentry-backtrace = { version = "0.45.0", path = "../sentry-backtrace", optional = true } +sentry-contexts = { version = "0.45.0", path = "../sentry-contexts", optional = true } +sentry-debug-images = { version = "0.45.0", path = "../sentry-debug-images", optional = true } +sentry-log = { version = "0.45.0", path = "../sentry-log", optional = true } +sentry-panic = { version = "0.45.0", path = "../sentry-panic", optional = true } +sentry-slog = { version = "0.45.0", path = "../sentry-slog", optional = true } +sentry-tower = { version = "0.45.0", path = "../sentry-tower", optional = true } +sentry-tracing = { version = "0.45.0", path = "../sentry-tracing", optional = true } +sentry-opentelemetry = { version = "0.45.0", path = "../sentry-opentelemetry", optional = true } reqwest = { version = "0.12", optional = true, features = [ "blocking", "json", ], default-features = false } curl = { version = "0.4.25", optional = true } httpdate = { version = "1.0.0", optional = true } -surf = { version = "2.0.0", optional = true, default-features = false } -http-client = { version = "6.5.3", optional = true } -isahc = { version = "0.9.14", optional = true } serde_json = { version = "1.0.48", optional = true } -tokio = { version = "1.0", features = ["rt"], optional = true } -ureq = { version = "2.7.0", optional = true, default-features = false } +tokio = { version = "1.44", features = ["rt"], optional = true } +ureq = { version = "3.0.11", optional = true, default-features = false } native-tls = { version = "0.2.8", optional = true } -rustls = { version = "0.21.2", optional = true, features = [ - "dangerous_configuration", -] } -webpki-roots = { version = "0.25.1", optional = true } +rustls = { version = "0.23.18", optional = true, default-features = false } +embedded-svc = { version = "0.28.1", optional = true } +[target.'cfg(target_os = "espidf")'.dependencies] +esp-idf-svc = { version = "0.51.0", optional = true } [dev-dependencies] sentry-anyhow = { path = "../sentry-anyhow" } @@ -94,7 +100,10 @@ anyhow = { version = "1.0.30" } log = { version = "0.4.8", features = ["std"] } pretty_env_logger = "0.5.0" slog = { version = "2.5.2" } -tokio = { version = "1.0", features = ["macros"] } -tower = { version = "0.4", features = ["util"] } +tokio = { version = "1.44", features = ["macros"] } +tower = { version = "0.5.2", features = ["util"] } tracing = { version = "0.1" } tracing-subscriber = { version = "0.3", features = ["fmt", "tracing-log"] } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(doc_cfg)'] } diff --git a/sentry/LICENSE b/sentry/LICENSE index d97d9399c..3e4988928 100644 --- a/sentry/LICENSE +++ b/sentry/LICENSE @@ -1,203 +1,21 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 Functional Software, Inc. dba Sentry (https://sentry.io) - and individual contributors. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2021 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sentry/README.md b/sentry/README.md index ba51bfcb3..5a6474d41 100644 --- a/sentry/README.md +++ b/sentry/README.md @@ -34,8 +34,8 @@ sentry::capture_message("Hello World!", sentry::Level::Info); More complex examples on how to use sentry can also be found in [examples]. Extended instructions may also be found on [Sentry itself]. -[`sentry::init`]: https://docs.rs/sentry/0.32.2/sentry/fn.init.html -[`Hub`]: https://docs.rs/sentry/0.32.2/sentry/struct.Hub.html +[`sentry::init`]: https://docs.rs/sentry/0.45.0/sentry/fn.init.html +[`Hub`]: https://docs.rs/sentry/0.45.0/sentry/struct.Hub.html [examples]: https://github.com/getsentry/sentry-rust/tree/master/sentry/examples [Sentry itself]: https://docs.sentry.io/platforms/rust @@ -47,8 +47,8 @@ the ecosystem require a feature flag. For available integrations and how to use [integrations] and [apply_defaults]. [Features]: #features -[integrations]: https://docs.rs/sentry/0.32.2/sentry/integrations/index.html -[apply_defaults]: https://docs.rs/sentry/0.32.2/sentry/fn.apply_defaults.html +[integrations]: https://docs.rs/sentry/0.45.0/sentry/integrations/index.html +[apply_defaults]: https://docs.rs/sentry/0.45.0/sentry/fn.apply_defaults.html ## Minimal API @@ -56,10 +56,11 @@ This crate comes fully-featured. If the goal is to instrument libraries for usag with sentry, or to extend sentry with a custom [`Integration`] or a [`Transport`], one should use the [`sentry-core`] crate instead. -[`Integration`]: https://docs.rs/sentry/0.32.2/sentry/trait.Integration.html -[`Transport`]: https://docs.rs/sentry/0.32.2/sentry/trait.Transport.html +[`Integration`]: https://docs.rs/sentry/0.45.0/sentry/trait.Integration.html +[`Transport`]: https://docs.rs/sentry/0.45.0/sentry/trait.Transport.html [`sentry-core`]: https://crates.io/crates/sentry-core + ## Features Additional functionality and integrations are enabled via feature flags. Some features require @@ -75,20 +76,24 @@ extra setup to function properly. | `test` | | | | | | `debug-images` | ✅ | 🔌 | | | | `log` | | 🔌 | | Requires extra setup; See [`sentry-log`]'s documentation. | -| `debug-logs` | | | ❗ | Requires extra setup; See [`sentry-log`]'s documentation. | | `slog` | | 🔌 | | Requires extra setup; See [`sentry-slog`]'s documentation. | | `reqwest` | ✅ | | | | | `native-tls` | ✅ | | | `reqwest` must be enabled. | | `rustls` | | | | `reqwest` must be enabled. `native-tls` must be disabled via `default-features = false`. | -| `curl` | | | | | -| `surf` | | | | | -| `tower` | | 🔌 | | Requires extra setup; See [`sentry-tower`]'s documentation. | | `ureq` | | | | `ureq` transport support using `rustls` by default | | `ureq-native-tls` | | | | | +| `curl` | | | | | +| `actix` | | 🔌 | | Requires extra setup; See [`sentry-actix`]'s documentation. | +| `tower` | | 🔌 | | Requires extra setup; See [`sentry-tower`]'s documentation. | +| `tracing` | | 🔌 | | Requires extra setup; See [`sentry-tracing`]'s documentation. | +| `opentelemetry` | | 🔌 | | Requires extra setup; See [`sentry-opentelemetry`]'s documentation. | [`sentry-log`]: https://crates.io/crates/sentry-log [`sentry-slog`]: https://crates.io/crates/sentry-slog +[`sentry-actix`]: https://crates.io/crates/sentry-actix [`sentry-tower`]: https://crates.io/crates/sentry-tower +[`sentry-tracing`]: https://crates.io/crates/sentry-tracing +[`sentry-opentelemetry`]: https://crates.io/crates/sentry-opentelemetry ### Default features - `backtrace`: Enables backtrace support. @@ -104,7 +109,6 @@ extra setup to function properly. ### Logging - `log`: Enables support for the `log` crate. - `slog`: Enables support for the `slog` crate. -- `debug-logs`: **Deprecated**. Uses the `log` crate for internal logging. ### Transports - `reqwest`: **Default**. Enables the `reqwest` transport. @@ -113,16 +117,18 @@ extra setup to function properly. feature, and `default-features = false` must be set to completely disable building `native-tls` dependencies. - `curl`: Enables the `curl` transport. -- `surf`: Enables the `surf` transport. - `ureq`: Enables the `ureq` transport using `rustls`. - `ureq-native-tls`: Enables the `ureq` transport using `native-tls`. ### Integrations +- `actix`: Enables support for the `actix-web` crate. - `tower`: Enables support for the `tower` crate and those using it. +- `tracing`: Enables support for the `tracing` crate and those using it. +- `opentelemetry`: Enables support for the `opentelemetry` and `opentelemetry-sdk` crates. ## Resources -License: Apache-2.0 +License: MIT - [Discord](https://discord.gg/ez5KZN7) server for project discussions. - Follow [@getsentry](https://twitter.com/getsentry) on Twitter for updates diff --git a/sentry/src/init.rs b/sentry/src/init.rs index befb66fd6..12527f0d3 100644 --- a/sentry/src/init.rs +++ b/sentry/src/init.rs @@ -1,6 +1,8 @@ use std::sync::Arc; -use sentry_core::{sentry_debug, SessionMode}; +use sentry_core::sentry_debug; +#[cfg(feature = "release-health")] +use sentry_core::SessionMode; use crate::defaults::apply_defaults; use crate::{Client, ClientOptions, Hub}; @@ -34,6 +36,7 @@ impl Drop for ClientInitGuard { sentry_debug!("dropping client guard (no client to dispose)"); } // end any session that might be open before closing the client + #[cfg(feature = "release-health")] crate::end_session(); self.0.close(None); } @@ -93,8 +96,12 @@ where C: Into, { let opts = apply_defaults(opts.into()); + + #[cfg(feature = "release-health")] let auto_session_tracking = opts.auto_session_tracking; + #[cfg(feature = "release-health")] let session_mode = opts.session_mode; + let client = Arc::new(Client::from(opts)); Hub::with(|hub| hub.bind_client(Some(client.clone()))); @@ -103,6 +110,7 @@ where } else { sentry_debug!("initialized disabled sentry client due to disabled or invalid DSN"); } + #[cfg(feature = "release-health")] if auto_session_tracking && session_mode == SessionMode::Application { crate::start_session() } diff --git a/sentry/src/lib.rs b/sentry/src/lib.rs index 6652e1800..a2d2d5178 100644 --- a/sentry/src/lib.rs +++ b/sentry/src/lib.rs @@ -52,6 +52,7 @@ //! [`Transport`]: trait.Transport.html //! [`sentry-core`]: https://crates.io/crates/sentry-core //! +//! //! # Features //! //! Additional functionality and integrations are enabled via feature flags. Some features require @@ -67,20 +68,24 @@ //! | `test` | | | | | //! | `debug-images` | ✅ | 🔌 | | | //! | `log` | | 🔌 | | Requires extra setup; See [`sentry-log`]'s documentation. | -//! | `debug-logs` | | | ❗ | Requires extra setup; See [`sentry-log`]'s documentation. | //! | `slog` | | 🔌 | | Requires extra setup; See [`sentry-slog`]'s documentation. | //! | `reqwest` | ✅ | | | | //! | `native-tls` | ✅ | | | `reqwest` must be enabled. | //! | `rustls` | | | | `reqwest` must be enabled. `native-tls` must be disabled via `default-features = false`. | -//! | `curl` | | | | | -//! | `surf` | | | | | -//! | `tower` | | 🔌 | | Requires extra setup; See [`sentry-tower`]'s documentation. | //! | `ureq` | | | | `ureq` transport support using `rustls` by default | //! | `ureq-native-tls` | | | | | +//! | `curl` | | | | | +//! | `actix` | | 🔌 | | Requires extra setup; See [`sentry-actix`]'s documentation. | +//! | `tower` | | 🔌 | | Requires extra setup; See [`sentry-tower`]'s documentation. | +//! | `tracing` | | 🔌 | | Requires extra setup; See [`sentry-tracing`]'s documentation. | +//! | `opentelemetry` | | 🔌 | | Requires extra setup; See [`sentry-opentelemetry`]'s documentation. | //! //! [`sentry-log`]: https://crates.io/crates/sentry-log //! [`sentry-slog`]: https://crates.io/crates/sentry-slog +//! [`sentry-actix`]: https://crates.io/crates/sentry-actix //! [`sentry-tower`]: https://crates.io/crates/sentry-tower +//! [`sentry-tracing`]: https://crates.io/crates/sentry-tracing +//! [`sentry-opentelemetry`]: https://crates.io/crates/sentry-opentelemetry //! //! ## Default features //! - `backtrace`: Enables backtrace support. @@ -96,7 +101,6 @@ //! ## Logging //! - `log`: Enables support for the `log` crate. //! - `slog`: Enables support for the `slog` crate. -//! - `debug-logs`: **Deprecated**. Uses the `log` crate for internal logging. //! //! ## Transports //! - `reqwest`: **Default**. Enables the `reqwest` transport. @@ -105,12 +109,14 @@ //! feature, and `default-features = false` must be set to completely disable building `native-tls` //! dependencies. //! - `curl`: Enables the `curl` transport. -//! - `surf`: Enables the `surf` transport. //! - `ureq`: Enables the `ureq` transport using `rustls`. //! - `ureq-native-tls`: Enables the `ureq` transport using `native-tls`. //! //! ## Integrations +//! - `actix`: Enables support for the `actix-web` crate. //! - `tower`: Enables support for the `tower` crate and those using it. +//! - `tracing`: Enables support for the `tracing` crate and those using it. +//! - `opentelemetry`: Enables support for the `opentelemetry` and `opentelemetry-sdk` crates. #![doc(html_favicon_url = "https://sentry-brand.storage.googleapis.com/favicon.ico")] #![doc(html_logo_url = "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png")] @@ -186,6 +192,10 @@ pub use crate::init::{init, ClientInitGuard}; /// [`ClientOptions::default_integrations`]: crate::ClientOptions::default_integrations /// [`apply_defaults()`]: ../fn.apply_defaults.html pub mod integrations { + #[cfg(feature = "actix")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "actix")))] + #[doc(inline)] + pub use sentry_actix as actix; #[cfg(feature = "anyhow")] #[cfg_attr(doc_cfg, doc(cfg(feature = "anyhow")))] #[doc(inline)] @@ -206,6 +216,10 @@ pub mod integrations { #[cfg_attr(doc_cfg, doc(cfg(feature = "log")))] #[doc(inline)] pub use sentry_log as log; + #[cfg(feature = "opentelemetry")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "opentelemetry")))] + #[doc(inline)] + pub use sentry_opentelemetry as opentelemetry; #[cfg(feature = "panic")] #[cfg_attr(doc_cfg, doc(cfg(feature = "panic")))] #[doc(inline)] diff --git a/sentry/src/transports/embedded_svc_http.rs b/sentry/src/transports/embedded_svc_http.rs new file mode 100644 index 000000000..f040cb957 --- /dev/null +++ b/sentry/src/transports/embedded_svc_http.rs @@ -0,0 +1,64 @@ +use crate::{sentry_debug, ClientOptions, Transport}; +use embedded_svc::http::client::Client as HttpClient; +use esp_idf_svc::{http::client::EspHttpConnection, io::Write}; + +/// Transport using the embedded-svc http client +pub struct EmbeddedSVCHttpTransport { + options: ClientOptions, +} + +impl EmbeddedSVCHttpTransport { + /// Creates a new transport + pub fn new(options: &ClientOptions) -> Self { + Self { + options: options.clone(), + } + } +} + +impl EmbeddedSVCHttpTransport { + fn send_envelope( + &self, + envelope: sentry_core::Envelope, + ) -> Result<(), Box> { + let dsn = self + .options + .dsn + .as_ref() + .ok_or_else(|| "No DSN specified")?; + let user_agent = &self.options.user_agent; + let auth = dsn.to_auth(Some(user_agent)).to_string(); + let headers = [("X-Sentry-Auth", auth.as_str())]; + let url = dsn.envelope_api_url(); + + let mut body = Vec::new(); + envelope.to_writer(&mut body)?; + + let config = esp_idf_svc::http::client::Configuration { + use_global_ca_store: true, + crt_bundle_attach: Some(esp_idf_svc::sys::esp_crt_bundle_attach), + ..Default::default() + }; + + let mut client = HttpClient::wrap(EspHttpConnection::new(&config)?); + + let mut request = client.post(url.as_str(), &headers)?; + request.write_all(&body)?; + request.flush()?; + let mut response = request.submit()?; + + // read the whole response + let mut buf = [0u8; 1024]; + while response.read(&mut buf)? > 0 {} + + Ok(()) + } +} + +impl Transport for EmbeddedSVCHttpTransport { + fn send_envelope(&self, envelope: sentry_core::Envelope) { + if let Err(err) = self.send_envelope(envelope) { + sentry_debug!("Failed to send envelope: {}", err); + } + } +} diff --git a/sentry/src/transports/mod.rs b/sentry/src/transports/mod.rs index 253c7fe0e..424c8a7a9 100644 --- a/sentry/src/transports/mod.rs +++ b/sentry/src/transports/mod.rs @@ -1,7 +1,7 @@ //! The provided transports. //! //! This module exposes all transports that are compiled into the sentry -//! library. The `reqwest`, `curl`, `surf` and `ureq` features turn on these transports. +//! library. The `reqwest`, `curl`, and `ureq` features turn on these transports. use crate::{ClientOptions, Transport, TransportFactory}; use std::sync::Arc; @@ -10,7 +10,7 @@ use std::sync::Arc; mod ratelimit; #[cfg(any(feature = "curl", feature = "ureq"))] mod thread; -#[cfg(any(feature = "reqwest", feature = "surf",))] +#[cfg(feature = "reqwest")] mod tokio_thread; #[cfg(feature = "reqwest")] @@ -18,16 +18,16 @@ mod reqwest; #[cfg(feature = "reqwest")] pub use self::reqwest::ReqwestHttpTransport; +#[cfg(all(target_os = "espidf", feature = "embedded-svc-http"))] +mod embedded_svc_http; +#[cfg(all(target_os = "espidf", feature = "embedded-svc-http"))] +pub use self::embedded_svc_http::EmbeddedSVCHttpTransport; + #[cfg(feature = "curl")] mod curl; #[cfg(feature = "curl")] pub use self::curl::CurlHttpTransport; -#[cfg(feature = "surf")] -mod surf; -#[cfg(feature = "surf")] -pub use self::surf::SurfHttpTransport; - #[cfg(feature = "ureq")] mod ureq; #[cfg(feature = "ureq")] @@ -38,33 +38,34 @@ type DefaultTransport = ReqwestHttpTransport; #[cfg(all( feature = "curl", + not(all(target_os = "espidf", feature = "embedded-svc-http")), not(feature = "reqwest"), - not(feature = "surf"), not(feature = "ureq") ))] type DefaultTransport = CurlHttpTransport; #[cfg(all( - feature = "surf", + feature = "ureq", + not(all(target_os = "espidf", feature = "embedded-svc-http")), not(feature = "reqwest"), not(feature = "curl"), - not(feature = "ureq") ))] -type DefaultTransport = SurfHttpTransport; +type DefaultTransport = UreqHttpTransport; #[cfg(all( - feature = "ureq", + target_os = "espidf", + feature = "embedded-svc-http", not(feature = "reqwest"), not(feature = "curl"), - not(feature = "surf") + not(feature = "ureq") ))] -type DefaultTransport = UreqHttpTransport; +type DefaultTransport = EmbeddedSVCHttpTransport; /// The default http transport. #[cfg(any( + all(target_os = "espidf", feature = "embedded-svc-http"), feature = "reqwest", feature = "curl", - feature = "surf", feature = "ureq" ))] pub type HttpTransport = DefaultTransport; @@ -80,18 +81,18 @@ pub struct DefaultTransportFactory; impl TransportFactory for DefaultTransportFactory { fn create_transport(&self, options: &ClientOptions) -> Arc { #[cfg(any( + all(target_os = "espidf", feature = "embedded-svc-http"), feature = "reqwest", feature = "curl", - feature = "surf", feature = "ureq" ))] { Arc::new(HttpTransport::new(options)) } #[cfg(not(any( + all(target_os = "espidf", feature = "embedded-svc-http"), feature = "reqwest", feature = "curl", - feature = "surf", feature = "ureq" )))] { diff --git a/sentry/src/transports/ratelimit.rs b/sentry/src/transports/ratelimit.rs index a64ce3a9f..fbe053590 100644 --- a/sentry/src/transports/ratelimit.rs +++ b/sentry/src/transports/ratelimit.rs @@ -2,6 +2,7 @@ use httpdate::parse_http_date; use std::time::{Duration, SystemTime}; use crate::protocol::EnvelopeItem; +use crate::protocol::ItemContainer; use crate::Envelope; /// A Utility that helps with rate limiting sentry requests. @@ -12,7 +13,7 @@ pub struct RateLimiter { session: Option, transaction: Option, attachment: Option, - statsd: Option, + log_item: Option, } impl RateLimiter { @@ -57,7 +58,7 @@ impl RateLimiter { "session" => self.session = new_time, "transaction" => self.transaction = new_time, "attachment" => self.attachment = new_time, - "statsd" => self.statsd = new_time, + "log_item" => self.log_item = new_time, _ => {} } } @@ -91,7 +92,7 @@ impl RateLimiter { RateLimitingCategory::Session => self.session, RateLimitingCategory::Transaction => self.transaction, RateLimitingCategory::Attachment => self.attachment, - RateLimitingCategory::Statsd => self.statsd, + RateLimitingCategory::LogItem => self.log_item, }?; time_left.duration_since(SystemTime::now()).ok() } @@ -115,8 +116,9 @@ impl RateLimiter { } EnvelopeItem::Transaction(_) => RateLimitingCategory::Transaction, EnvelopeItem::Attachment(_) => RateLimitingCategory::Attachment, - #[cfg(feature = "UNSTABLE_metrics")] - EnvelopeItem::Statsd(_) => RateLimitingCategory::Statsd, + EnvelopeItem::ItemContainer(ItemContainer::Logs(_)) => { + RateLimitingCategory::LogItem + } _ => RateLimitingCategory::Any, }) }) @@ -136,9 +138,8 @@ pub enum RateLimitingCategory { Transaction, /// Rate Limit pertaining to Attachments. Attachment, - /// Rate Limit pertaining to metrics. - #[allow(unused)] - Statsd, + /// Rate Limit pertaining to Log Items. + LogItem, } #[cfg(test)] @@ -153,6 +154,7 @@ mod tests { assert!(rl.is_disabled(RateLimitingCategory::Error).unwrap() <= Duration::from_secs(120)); assert!(rl.is_disabled(RateLimitingCategory::Session).unwrap() <= Duration::from_secs(60)); assert!(rl.is_disabled(RateLimitingCategory::Transaction).is_none()); + assert!(rl.is_disabled(RateLimitingCategory::LogItem).is_none()); assert!(rl.is_disabled(RateLimitingCategory::Any).is_none()); rl.update_from_sentry_header( @@ -169,6 +171,23 @@ mod tests { assert!(rl.is_disabled(RateLimitingCategory::Any).unwrap() <= Duration::from_secs(30)); } + #[test] + fn test_sentry_header_no_categories() { + let mut rl = RateLimiter::new(); + rl.update_from_sentry_header("120::bar"); + + assert!(rl.is_disabled(RateLimitingCategory::Error).unwrap() <= Duration::from_secs(120)); + assert!(rl.is_disabled(RateLimitingCategory::Session).unwrap() <= Duration::from_secs(120)); + assert!( + rl.is_disabled(RateLimitingCategory::Transaction).unwrap() <= Duration::from_secs(120) + ); + assert!(rl.is_disabled(RateLimitingCategory::LogItem).unwrap() <= Duration::from_secs(120)); + assert!( + rl.is_disabled(RateLimitingCategory::Attachment).unwrap() <= Duration::from_secs(120) + ); + assert!(rl.is_disabled(RateLimitingCategory::Any).unwrap() <= Duration::from_secs(120)); + } + #[test] fn test_retry_after() { let mut rl = RateLimiter::new(); diff --git a/sentry/src/transports/reqwest.rs b/sentry/src/transports/reqwest.rs index 8e79896f0..9e21364a0 100644 --- a/sentry/src/transports/reqwest.rs +++ b/sentry/src/transports/reqwest.rs @@ -53,7 +53,9 @@ impl ReqwestHttpTransport { } } }; - builder.build().unwrap() + builder + .build() + .expect("Failed to build `reqwest` client as a TLS backend is not available. Enable either the `native-tls` or the `rustls` feature of the `sentry` crate.") }); let dsn = options.dsn.as_ref().unwrap(); let user_agent = options.user_agent.clone(); diff --git a/sentry/src/transports/surf.rs b/sentry/src/transports/surf.rs deleted file mode 100644 index 0a902a688..000000000 --- a/sentry/src/transports/surf.rs +++ /dev/null @@ -1,102 +0,0 @@ -use std::time::Duration; - -use isahc::{ - config::{Configurable, SslOption}, - HttpClient, -}; -use surf::{http::headers as SurfHeaders, Client as SurfClient, StatusCode}; - -use super::tokio_thread::TransportThread; - -use crate::{sentry_debug, ClientOptions, Envelope, Transport}; - -/// A [`Transport`] that sends events via the [`surf`] library. -/// -/// This is enabled by the `surf` feature flag. -/// -/// [`surf`]: https://crates.io/crates/surf -#[cfg_attr(doc_cfg, doc(cfg(feature = "surf")))] -pub struct SurfHttpTransport { - thread: TransportThread, -} - -impl SurfHttpTransport { - /// Creates a new Transport. - pub fn new(options: &ClientOptions) -> Self { - Self::new_internal(options, None) - } - - /// Creates a new Transport that uses the specified [`SurfClient`]. - pub fn with_client(options: &ClientOptions, client: SurfClient) -> Self { - Self::new_internal(options, Some(client)) - } - - fn new_internal(options: &ClientOptions, client: Option) -> Self { - let mut client = client.unwrap_or_default(); - if options.accept_invalid_certs { - let hc = HttpClient::builder() - .ssl_options(SslOption::DANGER_ACCEPT_INVALID_CERTS) - .build() - .unwrap(); - let http_client = http_client::isahc::IsahcClient::from_client(hc); - client = SurfClient::with_http_client(http_client) - } - - let dsn = options.dsn.as_ref().unwrap(); - let user_agent = options.user_agent.clone(); - let auth = dsn.to_auth(Some(&user_agent)).to_string(); - let url = dsn.envelope_api_url().to_string(); - - let thread = TransportThread::new(move |envelope, mut rl| { - let mut body = Vec::new(); - envelope.to_writer(&mut body).unwrap(); - let request = client.post(&url).header("X-Sentry-Auth", &auth).body(body); - - async move { - match request.await { - Ok(mut response) => { - if let Some(sentry_header) = - response.header("x-sentry-rate-limits").map(|x| x.as_str()) - { - rl.update_from_retry_after(sentry_header); - } else if let Some(retry_after) = response - .header(SurfHeaders::RETRY_AFTER) - .map(|x| x.as_str()) - { - rl.update_from_retry_after(retry_after); - } else if response.status() == StatusCode::TooManyRequests { - rl.update_from_429(); - } - - match response.body_string().await { - Err(err) => { - sentry_debug!("Failed to read sentry response: {}", err); - } - Ok(text) => { - sentry_debug!("Get response: `{}`", text); - } - } - } - Err(err) => { - sentry_debug!("Failed to send envelope: {}", err); - } - } - rl - } - }); - Self { thread } - } -} - -impl Transport for SurfHttpTransport { - fn send_envelope(&self, envelope: Envelope) { - self.thread.send(envelope) - } - fn flush(&self, timeout: Duration) -> bool { - self.thread.flush(timeout) - } - - fn shutdown(&self, timeout: Duration) -> bool { - self.flush(timeout) - } -} diff --git a/sentry/src/transports/thread.rs b/sentry/src/transports/thread.rs index 368ecdd81..6ccca6fae 100644 --- a/sentry/src/transports/thread.rs +++ b/sentry/src/transports/thread.rs @@ -7,6 +7,11 @@ use std::time::Duration; use super::ratelimit::{RateLimiter, RateLimitingCategory}; use crate::{sentry_debug, Envelope}; +#[expect( + clippy::large_enum_variant, + reason = "In normal usage this is usually SendEnvelope, the other variants are only used when \ + the user manually calls transport.flush() or when the transport is shut down." +)] enum Task { SendEnvelope(Envelope), Flush(SyncSender<()>), @@ -74,13 +79,18 @@ impl TransportThread { } pub fn send(&self, envelope: Envelope) { - let _ = self.sender.send(Task::SendEnvelope(envelope)); + // Using send here would mean that when the channel fills up for whatever + // reason, trying to send an envelope would block everything. We'd rather + // drop the envelope in that case. + if let Err(e) = self.sender.try_send(Task::SendEnvelope(envelope)) { + sentry_debug!("envelope dropped: {e}"); + } } pub fn flush(&self, timeout: Duration) -> bool { let (sender, receiver) = sync_channel(1); let _ = self.sender.send(Task::Flush(sender)); - receiver.recv_timeout(timeout).is_err() + receiver.recv_timeout(timeout).is_ok() } } diff --git a/sentry/src/transports/tokio_thread.rs b/sentry/src/transports/tokio_thread.rs index 5ea1ae5e4..5ef734e31 100644 --- a/sentry/src/transports/tokio_thread.rs +++ b/sentry/src/transports/tokio_thread.rs @@ -7,6 +7,11 @@ use std::time::Duration; use super::ratelimit::{RateLimiter, RateLimitingCategory}; use crate::{sentry_debug, Envelope}; +#[expect( + clippy::large_enum_variant, + reason = "In normal usage this is usually SendEnvelope, the other variants are only used when \ + the user manually calls transport.flush() or when the transport is shut down." +)] enum Task { SendEnvelope(Envelope), Flush(SyncSender<()>), @@ -96,7 +101,7 @@ impl TransportThread { pub fn flush(&self, timeout: Duration) -> bool { let (sender, receiver) = sync_channel(1); let _ = self.sender.send(Task::Flush(sender)); - receiver.recv_timeout(timeout).is_err() + receiver.recv_timeout(timeout).is_ok() } } diff --git a/sentry/src/transports/ureq.rs b/sentry/src/transports/ureq.rs index ab4f45556..9fa20fe4b 100644 --- a/sentry/src/transports/ureq.rs +++ b/sentry/src/transports/ureq.rs @@ -1,18 +1,9 @@ -use std::sync::Arc; use std::time::Duration; -#[cfg(feature = "rustls")] -use std::time::SystemTime; - -#[cfg(feature = "native-tls")] -use native_tls::TlsConnector; -#[cfg(feature = "rustls")] -use rustls::{ - client::{ServerCertVerified, ServerCertVerifier}, - Certificate, ClientConfig, Error, OwnedTrustAnchor, RootCertStore, ServerName, -}; -use ureq::{Agent, AgentBuilder, Proxy}; -#[cfg(feature = "rustls")] -use webpki_roots::TLS_SERVER_ROOTS; + +use ureq::http::Response; +#[cfg(any(feature = "rustls", feature = "native-tls"))] +use ureq::tls::{TlsConfig, TlsProvider}; +use ureq::{Agent, Proxy}; use super::thread::TransportThread; @@ -41,61 +32,33 @@ impl UreqHttpTransport { let dsn = options.dsn.as_ref().unwrap(); let scheme = dsn.scheme(); let agent = agent.unwrap_or_else(|| { - let mut builder = AgentBuilder::new(); + let mut builder = Agent::config_builder(); #[cfg(feature = "native-tls")] { - let mut tls_connector_builder = TlsConnector::builder(); - - if options.accept_invalid_certs { - tls_connector_builder.danger_accept_invalid_certs(true); - } - - builder = builder.tls_connector(Arc::new(tls_connector_builder.build().unwrap())); + builder = builder.tls_config( + TlsConfig::builder() + .provider(TlsProvider::NativeTls) + .disable_verification(options.accept_invalid_certs) + .build(), + ); } - - if options.accept_invalid_certs { - #[cfg(feature = "rustls")] - { - struct NoVerifier; - - impl ServerCertVerifier for NoVerifier { - fn verify_server_cert( - &self, - _end_entity: &Certificate, - _intermediates: &[Certificate], - _server_name: &ServerName, - _scts: &mut dyn Iterator, - _ocsp_response: &[u8], - _now: SystemTime, - ) -> Result { - Ok(ServerCertVerified::assertion()) - } - } - - let mut root_store = RootCertStore::empty(); - root_store.add_trust_anchors(TLS_SERVER_ROOTS.iter().map(|ta| { - OwnedTrustAnchor::from_subject_spki_name_constraints( - ta.subject, - ta.spki, - ta.name_constraints, - ) - })); - let mut config = ClientConfig::builder() - .with_safe_defaults() - .with_root_certificates(root_store) - .with_no_client_auth(); - config - .dangerous() - .set_certificate_verifier(Arc::new(NoVerifier)); - builder = builder.tls_config(Arc::new(config)); - } + #[cfg(feature = "rustls")] + { + builder = builder.tls_config( + TlsConfig::builder() + .provider(TlsProvider::Rustls) + .disable_verification(options.accept_invalid_certs) + .build(), + ); } + let mut maybe_proxy = None; + match (scheme, &options.http_proxy, &options.https_proxy) { (Scheme::Https, _, Some(proxy)) => match Proxy::new(proxy) { Ok(proxy) => { - builder = builder.proxy(proxy); + maybe_proxy = Some(proxy); } Err(err) => { sentry_debug!("invalid proxy: {:?}", err); @@ -103,7 +66,7 @@ impl UreqHttpTransport { }, (_, Some(proxy), _) => match Proxy::new(proxy) { Ok(proxy) => { - builder = builder.proxy(proxy); + maybe_proxy = Some(proxy); } Err(err) => { sentry_debug!("invalid proxy: {:?}", err); @@ -112,7 +75,9 @@ impl UreqHttpTransport { _ => {} } - builder.build() + builder = builder.proxy(maybe_proxy); + + builder.build().new_agent() }); let user_agent = options.user_agent.clone(); let auth = dsn.to_auth(Some(&user_agent)).to_string(); @@ -121,22 +86,23 @@ impl UreqHttpTransport { let thread = TransportThread::new(move |envelope, rl| { let mut body = Vec::new(); envelope.to_writer(&mut body).unwrap(); - let request = agent - .post(&url) - .set("X-Sentry-Auth", &auth) - .send_bytes(&body); + let request = agent.post(&url).header("X-Sentry-Auth", &auth).send(&body); match request { - Ok(response) => { - if let Some(sentry_header) = response.header("x-sentry-rate-limits") { + Ok(mut response) => { + fn header_str<'a, B>(response: &'a Response, key: &str) -> Option<&'a str> { + response.headers().get(key)?.to_str().ok() + } + + if let Some(sentry_header) = header_str(&response, "x-sentry-rate-limits") { rl.update_from_sentry_header(sentry_header); - } else if let Some(retry_after) = response.header("retry-after") { + } else if let Some(retry_after) = header_str(&response, "retry-after") { rl.update_from_retry_after(retry_after); } else if response.status() == 429 { rl.update_from_429(); } - match response.into_string() { + match response.body_mut().read_to_string() { Err(err) => { sentry_debug!("Failed to read sentry response: {}", err); } diff --git a/sentry/tests/test_basic.rs b/sentry/tests/test_basic.rs index 3813b14ea..21cb98ab3 100644 --- a/sentry/tests/test_basic.rs +++ b/sentry/tests/test_basic.rs @@ -3,8 +3,10 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; -use sentry::protocol::{Attachment, EnvelopeItem}; -use sentry::types::Uuid; +use sentry::protocol::{ + Attachment, Context, DynamicSamplingContext, EnvelopeHeaders, EnvelopeItem, +}; +use sentry::types::{Dsn, Uuid}; #[test] fn test_basic_capture_message() { @@ -28,6 +30,25 @@ fn test_basic_capture_message() { assert_eq!(Some(event.event_id), last_event_id); } +#[test] +fn test_event_trace_context_from_propagation_context() { + let mut last_event_id = None::; + let mut span = None; + let events = sentry::test::with_captured_events(|| { + sentry::configure_scope(|scope| { + span = scope.get_span(); + }); + sentry::capture_message("Hello World!", sentry::Level::Warning); + last_event_id = sentry::last_event_id(); + }); + assert_eq!(events.len(), 1); + let event = events.into_iter().next().unwrap(); + + let trace_context = event.contexts.get("trace"); + assert!(span.is_none()); + assert!(matches!(trace_context, Some(Context::Trace(_)))); +} + #[test] fn test_breadcrumbs() { let events = sentry::test::with_captured_events(|| { @@ -245,3 +266,353 @@ fn test_panic_scope_pop() { Some("Popped scope guard out of order".into()) ); } + +#[cfg(feature = "logs")] +#[test] +fn test_basic_capture_log() { + use std::time::SystemTime; + + use sentry::{protocol::Log, protocol::LogAttribute, protocol::Map, Hub}; + + let options = sentry::ClientOptions { + enable_logs: true, + ..Default::default() + }; + let envelopes = sentry::test::with_captured_envelopes_options( + || { + let mut attributes: Map = Map::new(); + attributes.insert("test".into(), "a string".into()); + let log = Log { + level: sentry::protocol::LogLevel::Warn, + body: "this is a test".into(), + trace_id: None, + timestamp: SystemTime::now(), + severity_number: None, + attributes, + }; + + Hub::current().capture_log(log); + }, + options, + ); + assert_eq!(envelopes.len(), 1); + let envelope = envelopes.first().expect("expected envelope"); + let item = envelope.items().next().expect("expected envelope item"); + match item { + EnvelopeItem::ItemContainer(container) => match container { + sentry::protocol::ItemContainer::Logs(logs) => { + let log = logs.iter().next().expect("expected log"); + assert_eq!(sentry::protocol::LogLevel::Warn, log.level); + assert_eq!("this is a test", log.body); + assert!(log.trace_id.is_some()); + assert!(log.severity_number.is_none()); + assert!(log.attributes.contains_key("sentry.sdk.name")); + assert!(log.attributes.contains_key("sentry.sdk.version")); + assert!(log.attributes.contains_key("test")); + } + _ => panic!("expected logs"), + }, + _ => panic!("expected item container"), + } +} + +#[cfg(feature = "logs")] +#[test] +fn test_basic_capture_log_macro_message() { + use sentry_core::logger_info; + + let options = sentry::ClientOptions { + enable_logs: true, + ..Default::default() + }; + let envelopes = sentry::test::with_captured_envelopes_options( + || { + logger_info!("Hello, world!"); + }, + options, + ); + assert_eq!(envelopes.len(), 1); + let envelope = envelopes.first().expect("expected envelope"); + let item = envelope.items().next().expect("expected envelope item"); + match item { + EnvelopeItem::ItemContainer(container) => match container { + sentry::protocol::ItemContainer::Logs(logs) => { + let log = logs.iter().next().expect("expected log"); + assert_eq!(sentry_core::protocol::LogLevel::Info, log.level); + assert_eq!("Hello, world!", log.body); + assert!(log.trace_id.is_some()); + assert!(log.severity_number.is_none()); + assert!(log.attributes.contains_key("sentry.sdk.name")); + assert!(log.attributes.contains_key("sentry.sdk.version")); + } + _ => panic!("expected logs"), + }, + _ => panic!("expected item container"), + } +} + +#[cfg(feature = "logs")] +#[test] +fn test_basic_capture_log_macro_message_formatted() { + use sentry::protocol::LogAttribute; + use sentry_core::logger_warn; + + let options = sentry::ClientOptions { + enable_logs: true, + ..Default::default() + }; + let envelopes = sentry::test::with_captured_envelopes_options( + || { + let failed_requests = ["request1", "request2", "request3"]; + logger_warn!( + "Critical system errors detected for user {}, total failures: {}", + "test_user", + failed_requests.len() + ); + }, + options, + ); + assert_eq!(envelopes.len(), 1); + let envelope = envelopes.first().expect("expected envelope"); + let item = envelope.items().next().expect("expected envelope item"); + match item { + EnvelopeItem::ItemContainer(container) => match container { + sentry::protocol::ItemContainer::Logs(logs) => { + let log = logs.iter().next().expect("expected log"); + assert_eq!(sentry_core::protocol::LogLevel::Warn, log.level); + assert_eq!( + "Critical system errors detected for user test_user, total failures: 3", + log.body + ); + assert_eq!( + LogAttribute::from( + "Critical system errors detected for user {}, total failures: {}" + ), + log.attributes + .get("sentry.message.template") + .unwrap() + .clone() + ); + assert_eq!( + LogAttribute::from("test_user"), + log.attributes + .get("sentry.message.parameter.0") + .unwrap() + .clone() + ); + assert_eq!( + LogAttribute::from(3), + log.attributes + .get("sentry.message.parameter.1") + .unwrap() + .clone() + ); + assert!(log.trace_id.is_some()); + assert!(log.severity_number.is_none()); + assert!(log.attributes.contains_key("sentry.sdk.name")); + assert!(log.attributes.contains_key("sentry.sdk.version")); + } + _ => panic!("expected logs"), + }, + _ => panic!("expected item container"), + } +} + +#[cfg(feature = "logs")] +#[test] +fn test_basic_capture_log_macro_message_with_attributes() { + use sentry::protocol::LogAttribute; + use sentry_core::logger_error; + + let options = sentry::ClientOptions { + enable_logs: true, + ..Default::default() + }; + let envelopes = sentry::test::with_captured_envelopes_options( + || { + logger_error!( + user.id = "12345", + user.active = true, + request.duration = 150, + success = false, + "Failed to process request" + ); + }, + options, + ); + assert_eq!(envelopes.len(), 1); + let envelope = envelopes.first().expect("expected envelope"); + let item = envelope.items().next().expect("expected envelope item"); + match item { + EnvelopeItem::ItemContainer(container) => match container { + sentry::protocol::ItemContainer::Logs(logs) => { + let log = logs.iter().next().expect("expected log"); + assert_eq!(sentry_core::protocol::LogLevel::Error, log.level); + assert_eq!("Failed to process request", log.body); + assert_eq!(None, log.attributes.get("sentry.message.template")); + assert!(log.trace_id.is_some()); + assert!(log.severity_number.is_none()); + assert!(log.attributes.contains_key("sentry.sdk.name")); + assert!(log.attributes.contains_key("sentry.sdk.version")); + assert_eq!( + LogAttribute::from("12345"), + log.attributes.get("user.id").unwrap().clone() + ); + assert_eq!( + LogAttribute::from(true), + log.attributes.get("user.active").unwrap().clone() + ); + assert_eq!( + LogAttribute::from(150u64), + log.attributes.get("request.duration").unwrap().clone() + ); + assert_eq!( + LogAttribute::from(false), + log.attributes.get("success").unwrap().clone() + ); + } + _ => panic!("expected logs"), + }, + _ => panic!("expected item container"), + } +} + +#[cfg(feature = "logs")] +#[test] +fn test_basic_capture_log_macro_message_formatted_with_attributes() { + use sentry::protocol::LogAttribute; + use sentry_core::logger_debug; + + let options = sentry::ClientOptions { + enable_logs: true, + ..Default::default() + }; + let envelopes = sentry::test::with_captured_envelopes_options( + || { + logger_debug!( + hello = "test", + operation.name = "database_query", + operation.success = true, + operation.time_ms = 42, + world = 10, + "Database query {} completed in {} ms with {} results", + "users_by_region", + 42, + 15 + ); + }, + options, + ); + assert_eq!(envelopes.len(), 1); + let envelope = envelopes.first().expect("expected envelope"); + let item = envelope.items().next().expect("expected envelope item"); + match item { + EnvelopeItem::ItemContainer(container) => match container { + sentry::protocol::ItemContainer::Logs(logs) => { + let log = logs.iter().next().expect("expected log"); + assert_eq!(sentry_core::protocol::LogLevel::Debug, log.level); + assert_eq!( + "Database query users_by_region completed in 42 ms with 15 results", + log.body + ); + assert!(log.trace_id.is_some()); + assert!(log.severity_number.is_none()); + assert_eq!( + LogAttribute::from("Database query {} completed in {} ms with {} results",), + log.attributes + .get("sentry.message.template") + .unwrap() + .clone() + ); + assert!(log.attributes.contains_key("sentry.sdk.name")); + assert!(log.attributes.contains_key("sentry.sdk.version")); + assert_eq!( + LogAttribute::from("test"), + log.attributes.get("hello").unwrap().clone() + ); + assert_eq!( + LogAttribute::from("database_query"), + log.attributes.get("operation.name").unwrap().clone() + ); + assert_eq!( + LogAttribute::from(true), + log.attributes.get("operation.success").unwrap().clone() + ); + assert_eq!( + LogAttribute::from(42u64), + log.attributes.get("operation.time_ms").unwrap().clone() + ); + assert_eq!( + LogAttribute::from(10), + log.attributes.get("world").unwrap().clone() + ); + assert_eq!( + LogAttribute::from("Database query {} completed in {} ms with {} results"), + log.attributes + .get("sentry.message.template") + .unwrap() + .clone() + ); + assert_eq!( + LogAttribute::from("users_by_region"), + log.attributes + .get("sentry.message.parameter.0") + .unwrap() + .clone() + ); + assert_eq!( + LogAttribute::from(42), + log.attributes + .get("sentry.message.parameter.1") + .unwrap() + .clone() + ); + assert_eq!( + LogAttribute::from(15), + log.attributes + .get("sentry.message.parameter.2") + .unwrap() + .clone() + ); + } + _ => panic!("expected logs"), + }, + _ => panic!("expected item container"), + } +} + +#[test] +fn test_transaction_envelope_dsc_headers() { + let mut trace_id: Option = None; + let dsn: Option = "http://foo@example.com/42".parse().ok(); + let envelopes = sentry::test::with_captured_envelopes_options( + || { + let transaction_ctx = sentry::TransactionContext::new("name transaction", "op"); + trace_id = Some(transaction_ctx.trace_id()); + let transaction = sentry::start_transaction(transaction_ctx); + sentry::configure_scope(|scope| scope.set_span(Some(transaction.clone().into()))); + transaction.finish(); + }, + sentry::ClientOptions { + dsn: dsn.clone(), + traces_sample_rate: 1.0, + ..Default::default() + }, + ); + + assert!(trace_id.is_some()); + let trace_id = trace_id.unwrap(); + assert_eq!(envelopes.len(), 1); + let envelope = envelopes.into_iter().next().unwrap(); + assert!(envelope.uuid().is_some()); + let uuid = envelope.uuid().copied().unwrap(); + + let expected = EnvelopeHeaders::new().with_event_id(uuid).with_trace( + DynamicSamplingContext::new() + .with_trace_id(trace_id) + .with_public_key(dsn.unwrap().public_key().to_owned()) + .with_sample_rate(1.0) + .with_sampled(true), + ); + assert_eq!(envelope.headers(), &expected); +} diff --git a/sentry/tests/test_log_combined_filters.rs b/sentry/tests/test_log_combined_filters.rs new file mode 100644 index 000000000..3342743ba --- /dev/null +++ b/sentry/tests/test_log_combined_filters.rs @@ -0,0 +1,37 @@ +#![cfg(feature = "test")] + +// Test `log` integration with combined `LogFilter`s. +// This must be in a separate file because `log::set_boxed_logger` can only be called once. + +#[test] +fn test_log_combined_filters() { + let logger = sentry_log::SentryLogger::new().filter(|md| match md.level() { + log::Level::Error => sentry_log::LogFilter::Breadcrumb | sentry_log::LogFilter::Event, + log::Level::Warn => sentry_log::LogFilter::Event, + _ => sentry_log::LogFilter::Ignore, + }); + + log::set_boxed_logger(Box::new(logger)) + .map(|()| log::set_max_level(log::LevelFilter::Trace)) + .unwrap(); + + let events = sentry::test::with_captured_events(|| { + log::error!("Both a breadcrumb and an event"); + log::warn!("An event"); + log::trace!("Ignored"); + }); + + assert_eq!(events.len(), 2); + + assert_eq!( + events[0].message, + Some("Both a breadcrumb and an event".to_owned()) + ); + + assert_eq!(events[1].message, Some("An event".to_owned())); + assert_eq!(events[1].breadcrumbs.len(), 1); + assert_eq!( + events[1].breadcrumbs[0].message, + Some("Both a breadcrumb and an event".into()) + ); +} diff --git a/sentry/tests/test_log_logs.rs b/sentry/tests/test_log_logs.rs new file mode 100644 index 000000000..881ecd25f --- /dev/null +++ b/sentry/tests/test_log_logs.rs @@ -0,0 +1,61 @@ +#![cfg(feature = "test")] + +// Test `log` integration <> Sentry structured logging. +// This must be a in a separate file because `log::set_boxed_logger` can only be called once. +#[cfg(feature = "logs")] +#[test] +fn test_log_logs() { + let logger = sentry_log::SentryLogger::new().filter(|_| sentry_log::LogFilter::Log); + + log::set_boxed_logger(Box::new(logger)) + .map(|()| log::set_max_level(log::LevelFilter::Trace)) + .unwrap(); + + let options = sentry::ClientOptions { + enable_logs: true, + ..Default::default() + }; + + let envelopes = sentry::test::with_captured_envelopes_options( + || { + log::info!(user_id = 42, request_id = "abc123"; "This is a log"); + }, + options, + ); + + assert_eq!(envelopes.len(), 1); + let envelope = envelopes.first().expect("expected envelope"); + let item = envelope.items().next().expect("expected envelope item"); + + match item { + sentry::protocol::EnvelopeItem::ItemContainer(container) => match container { + sentry::protocol::ItemContainer::Logs(logs) => { + assert_eq!(logs.len(), 1); + + let info_log = logs + .iter() + .find(|log| log.level == sentry::protocol::LogLevel::Info) + .expect("expected info log"); + assert_eq!(info_log.body, "This is a log"); + assert_eq!( + info_log.attributes.get("user_id").unwrap().clone(), + 42.into() + ); + assert_eq!( + info_log.attributes.get("request_id").unwrap().clone(), + "abc123".into() + ); + assert_eq!( + info_log.attributes.get("logger.target").unwrap().clone(), + "test_log_logs".into() + ); + assert_eq!( + info_log.attributes.get("sentry.origin").unwrap().clone(), + "auto.log.log".into() + ); + } + _ => panic!("expected logs"), + }, + _ => panic!("expected item container"), + } +} diff --git a/sentry/tests/test_tracing.rs b/sentry/tests/test_tracing.rs index 49e2dd305..f71e5b231 100644 --- a/sentry/tests/test_tracing.rs +++ b/sentry/tests/test_tracing.rs @@ -28,6 +28,7 @@ fn test_tracing() { let err = "NaN".parse::().unwrap_err(); let err: &dyn std::error::Error = &err; + tracing::warn!(something = err, "Breadcrumb with error"); tracing::error!(err, tagname = "tagvalue"); let _ = fn_errors(); }); @@ -78,6 +79,7 @@ fn test_tracing() { ); let event = events.next().unwrap(); + assert_eq!(event.breadcrumbs.len(), 3); assert!(!event.exception.is_empty()); assert_eq!(event.exception[0].ty, "ParseIntError"); assert_eq!( @@ -100,6 +102,17 @@ fn test_tracing() { _ => panic!("Wrong context type"), } + assert_eq!(event.breadcrumbs[2].level, sentry::Level::Warning); + assert_eq!( + event.breadcrumbs[2].message, + Some("Breadcrumb with error".into()) + ); + assert!(event.breadcrumbs[2].data.contains_key("something")); + assert_eq!( + event.breadcrumbs[2].data.get("something").unwrap(), + &Value::from(vec!("ParseIntError: invalid digit found in string")) + ); + let event = events.next().unwrap(); assert_eq!(event.message, Some("I'm broken!".to_string())); } @@ -180,3 +193,168 @@ fn test_set_transaction() { assert_eq!(transaction.name.as_deref().unwrap(), "new name"); assert!(transaction.request.is_some()); } + +#[cfg(feature = "logs")] +#[test] +fn test_tracing_logs() { + let sentry_layer = sentry_tracing::layer().event_filter(|_| sentry_tracing::EventFilter::Log); + + let _dispatcher = tracing_subscriber::registry() + .with(sentry_layer) + .set_default(); + + let options = sentry::ClientOptions { + enable_logs: true, + ..Default::default() + }; + + let envelopes = sentry::test::with_captured_envelopes_options( + || { + #[derive(Debug)] + struct ConnectionError { + message: String, + source: Option, + } + + impl std::fmt::Display for ConnectionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } + } + + impl std::error::Error for ConnectionError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.source + .as_ref() + .map(|e| e as &(dyn std::error::Error + 'static)) + } + } + + let io_error = + std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "Connection refused"); + let connection_error = ConnectionError { + message: "Failed to connect to database server".to_string(), + source: Some(io_error), + }; + + tracing::error!( + my.key = "hello", + an.error = &connection_error as &dyn std::error::Error, + "This is an error log: {}", + "hello" + ); + }, + options, + ); + + assert_eq!(envelopes.len(), 1); + let envelope = envelopes.first().expect("expected envelope"); + let item = envelope.items().next().expect("expected envelope item"); + + match item { + sentry::protocol::EnvelopeItem::ItemContainer(container) => match container { + sentry::protocol::ItemContainer::Logs(logs) => { + assert_eq!(logs.len(), 1); + + let log = &logs[0]; + assert_eq!(log.level, sentry::protocol::LogLevel::Error); + assert_eq!(log.body, "This is an error log: hello"); + assert!(log.trace_id.is_some()); + assert_eq!( + log.attributes.get("my.key").unwrap().clone(), + sentry::protocol::LogAttribute::from("hello") + ); + assert_eq!( + log.attributes.get("an.error").unwrap().clone(), + sentry::protocol::LogAttribute::from(vec![ + "ConnectionError: Failed to connect to database server", + "Custom: Connection refused" + ]) + ); + } + _ => panic!("expected logs container"), + }, + _ => panic!("expected item container"), + } +} + +#[test] +fn test_combined_event_filters() { + let sentry_layer = sentry_tracing::layer().event_filter(|md| match *md.level() { + tracing::Level::ERROR => { + sentry_tracing::EventFilter::Breadcrumb | sentry_tracing::EventFilter::Event + } + tracing::Level::WARN => sentry_tracing::EventFilter::Event, + _ => sentry_tracing::EventFilter::Ignore, + }); + + let _dispatcher = tracing_subscriber::registry() + .with(sentry_layer) + .set_default(); + + let events = sentry::test::with_captured_events(|| { + tracing::error!("Both a breadcrumb and an event"); + tracing::warn!("An event"); + }); + + assert_eq!(events.len(), 2); + + assert_eq!( + events[0].message, + Some("Both a breadcrumb and an event".to_owned()) + ); + + assert_eq!(events[1].message, Some("An event".to_owned())); + assert_eq!(events[1].breadcrumbs.len(), 1); + assert_eq!( + events[1].breadcrumbs[0].message, + Some("Both a breadcrumb and an event".into()) + ); +} + +#[test] +fn test_combined_event_mapper() { + let sentry_layer = + sentry_tracing::layer().event_mapper(|event, ctx| match *event.metadata().level() { + tracing::Level::ERROR => { + let breadcrumb = sentry_tracing::breadcrumb_from_event(event, Some(&ctx)); + let sentry_event = sentry_tracing::event_from_event(event, Some(&ctx)); + + sentry_tracing::EventMapping::Combined( + vec![ + sentry_tracing::EventMapping::Breadcrumb(breadcrumb), + sentry_tracing::EventMapping::Event(sentry_event), + ] + .into(), + ) + } + tracing::Level::WARN => { + let sentry_event = sentry_tracing::event_from_event(event, Some(&ctx)); + sentry_tracing::EventMapping::Event(sentry_event) + } + _ => sentry_tracing::EventMapping::Ignore, + }); + + let _dispatcher = tracing_subscriber::registry() + .with(sentry_layer) + .set_default(); + + let events = sentry::test::with_captured_events(|| { + tracing::error!("Both a breadcrumb and an event"); + tracing::warn!("An event"); + }); + + assert_eq!(events.len(), 2); + + assert_eq!( + events[0].message, + Some("Both a breadcrumb and an event".to_owned()) + ); + + assert_eq!(events[1].message, Some("An event".to_owned())); + assert_eq!(events[1].breadcrumbs.len(), 1); + assert_eq!( + events[1].breadcrumbs[0].message, + Some("Both a breadcrumb and an event".into()) + ); +}