Skip to content

feat: opt-in tracing instrumentation on generated entity methods #118

@RAprogramm

Description

@RAprogramm

Goal

Make logger output pinpoint which entity failed without users writing per-call boilerplate. Every generated async method (CRUD on `Repository`, transaction adapter, projection lookups, etc.) gets a `tracing` span carrying the entity name and operation. Errors emitted inside the method inherit that context automatically.

Design

Opt-in via a Cargo feature so users who don't depend on `tracing` don't pay for it.

Cargo wiring

  • `crates/entity-derive/Cargo.toml` — add feature `tracing = ["dep:tracing", "entity-core/tracing"]` and an optional `tracing` dep.
  • `crates/entity-core/Cargo.toml` — add feature `tracing = ["dep:tracing"]` and an optional `tracing` dep. Used to instrument `Transaction::run` and `TransactionContext::commit` / `rollback` in entity-core itself.
  • No change to `entity-derive-impl/Cargo.toml` (proc-macro has no runtime deps; it just emits the attribute).

Generated code

Every generated async method is wrapped with:

```rust
#[cfg_attr(
feature = "tracing",
tracing::instrument(
skip_all,
fields(entity = "User", op = "create"),
err,
)
)]
async fn create(&self, dto: CreateUserRequest) -> Result<User, sqlx::Error> { … }
```

`skip_all` keeps DTOs and connection handles out of the span fields by default — only `entity` and `op` are recorded. `err` automatically emits an `ERROR` event when the method returns `Err`, with the formatted error in the message.

Methods covered:

  • Repository CRUD: `create`, `update`, `delete`, `find_by_id`, `list`, plus generated filter/projection/lookup methods.
  • Transaction adapter (`TransactionRepo`): same set, fields say `source = "transaction"`.
  • `entity-core::Transaction::run` and `run_with_commit`: top-level span with `entity = "", op = "run"` so user spans nest under it.

Module/operation naming

  • `fields(entity = "User")` — `stringify!(#entity_name)`.
  • `fields(op = "create")` — fixed per generator.
  • `name = "entity.."` — explicit span name (avoids the default `module_path!`).

User-side usage

```toml
[dependencies]
entity-derive = { version = "…", features = ["tracing"] }
tracing = "0.1"
tracing-subscriber = "0.3" # or whatever subscriber the user wants
```

That's it. No code changes required beyond initializing a subscriber. Without the `tracing` feature, generated code is bit-for-bit identical to today.

Tests

  • Unit test in `entity-derive-impl` verifying the proc-macro emits `#[cfg_attr(feature = "tracing", tracing::instrument(...))]` with the expected `entity`/`op` fields. Token-string comparison.
  • Doctest / example update in `examples/transactions` showing how a panic deep inside a transaction surfaces with entity context.

Out of scope

  • Custom `EntityError` type (option 3 — would be a breaking change deferred to 0.8.0).
  • Recording arbitrary DTO fields in spans (`skip_all` keeps things conservative; users can pass `Display` impls later if desired).

Acceptance

Building any `#[derive(Entity)]` user crate with the `tracing` feature on and a `tracing-subscriber` configured produces log output of the shape:

```
ERROR entity.User.create: error=database error: duplicate key value violates unique constraint
in entity.User.create with entity="User" op="create"
```

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions