{"id":49301046,"url":"https://github.com/o6lvl4/sashiko","last_synced_at":"2026-05-01T12:01:10.516Z","repository":{"id":353587433,"uuid":"1219979636","full_name":"O6lvl4/sashiko","owner":"O6lvl4","description":"Concurrency-boundary observability for Ruby on top of OpenTelemetry — Thread / Fiber / queue / Ractor handoffs that vanilla OTel drops, plus a declarative trace DSL and tracer: DI for Box-aware multi-tenancy.","archived":false,"fork":false,"pushed_at":"2026-04-26T07:00:34.000Z","size":1103,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-29T10:36:53.559Z","etag":null,"topics":["concurrency","distributed-tracing","observability","opentelemetry","otel","ractor","ruby","ruby-box","ruby4","tracing"],"latest_commit_sha":null,"homepage":"https://o6lvl4.github.io/sashiko/","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/O6lvl4.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-24T12:22:30.000Z","updated_at":"2026-04-28T09:28:02.000Z","dependencies_parsed_at":null,"dependency_job_id":"530732e5-b3c0-492b-a75b-b0d9543a52bd","html_url":"https://github.com/O6lvl4/sashiko","commit_stats":null,"previous_names":["o6lvl4/sashiko"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/O6lvl4/sashiko","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/O6lvl4%2Fsashiko","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/O6lvl4%2Fsashiko/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/O6lvl4%2Fsashiko/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/O6lvl4%2Fsashiko/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/O6lvl4","download_url":"https://codeload.github.com/O6lvl4/sashiko/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/O6lvl4%2Fsashiko/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32462304,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-29T22:27:22.272Z","status":"online","status_checked_at":"2026-04-30T02:00:05.929Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["concurrency","distributed-tracing","observability","opentelemetry","otel","ractor","ruby","ruby-box","ruby4","tracing"],"created_at":"2026-04-26T07:00:56.931Z","updated_at":"2026-04-30T11:01:16.885Z","avatar_url":"https://github.com/O6lvl4.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Sashiko\n\n**Concurrency-boundary observability for Ruby on top of OpenTelemetry.**\n\nThe OTel context is fiber-local. The moment work crosses a Thread, Fiber,\nqueue, or Ractor boundary, vanilla OpenTelemetry Ruby drops the trace —\nspans from the spawned work become *root spans*, disconnected from the\nrequest that started them. Sashiko keeps the trace stitched together\nacross every concurrency boundary Ruby has, with a small declarative\nDSL for instrumenting your own code.\n\n[![Test](https://github.com/O6lvl4/sashiko/actions/workflows/test.yml/badge.svg)](https://github.com/O6lvl4/sashiko/actions/workflows/test.yml)\n[![Typecheck](https://github.com/O6lvl4/sashiko/actions/workflows/typecheck.yml/badge.svg)](https://github.com/O6lvl4/sashiko/actions/workflows/typecheck.yml)\n[![Docs](https://github.com/O6lvl4/sashiko/actions/workflows/docs.yml/badge.svg)](https://o6lvl4.github.io/sashiko/)\n\nAPI docs: \u003chttps://o6lvl4.github.io/sashiko/\u003e\n\n\u003e Status: early. API may change. Tests green in both default and\n\u003e `RUBY_BOX=1` modes; 0 type errors.\n\nNamed after *sashiko* (刺し子), a Japanese stitching technique that\nreinforces fabric with small, deliberate stitches.\n\n## The boundary problem\n\n```\nWithout Sashiko:                          With Sashiko:\n\n[trace A]                                 [trace A]\n└─ POST /orders                           └─ POST /orders\n                                             ├─ external_call\n[trace B]   ← orphan thread span 😢          ├─ external_call\n└─ external_call                             └─ external_call\n[trace C]   ← orphan thread span 😢\n└─ external_call\n```\n\nA 30-line runnable demonstration is in\n[`examples/thread_fanout_demo.rb`](examples/thread_fanout_demo.rb).\nIt prints both trace trees side-by-side from the same code.\n\n## What it gives you\n\n- **Boundary-aware Context helpers** — `Sashiko::Context.thread`,\n  `.fiber`, `.parallel_map` preserve the OTel Context across Ruby's\n  in-process concurrency primitives. `Sashiko::Context.carrier` returns\n  a deep-frozen, Ractor-shareable Hash of W3C Trace Context headers\n  that survives Sidekiq job args, Kafka message attributes, HTTP\n  headers, and `Ractor.new(...)` arguments — `Sashiko::Context.attach`\n  reconnects the trace on the other side.\n- **Declarative span DSL** — `extend Sashiko::Traced; trace :method`\n  instead of `tracer.in_span { ... }` blocks. Exceptions and error\n  status are recorded automatically.\n- **Spans from inside a Ractor** — vanilla OTel Ruby raises\n  `Ractor::IsolationError` on `tracer.in_span` inside a Ractor.\n  Sashiko records the work as plain `SpanEvent` data inside the\n  Ractor and *replays* it as real OTel spans on the main Ractor,\n  with the original timestamps and parent linkage. Caveats below.\n- **Per-Box (per-tenant) instrumentation** — under `RUBY_BOX=1`, each\n  `Sashiko::Box.new` gets its own Sashiko and OTel SDK. Useful for\n  multi-tenant processes; see the Box section for known constraints.\n- **Tracer DI everywhere** — every public entry point accepts a\n  `tracer:` keyword that bypasses the global memoized tracer. The\n  recommended escape hatch for routing spans through a non-default\n  provider (Box-local, alt backend, test fixture).\n- **Typed** — ships RBS signatures, type-checked with Steep in CI.\n\nSashiko is intended as a **companion** to the SIG-maintained\n`opentelemetry-instrumentation-*` gems, not a replacement. Use those\nfor Rails / Sidekiq / Faraday / etc.; reach for Sashiko for the\ncustom-code parts and the boundary handoffs they don't cover.\n\n## When to use Sashiko\n\nOpenTelemetry has three signal types — *traces*, *metrics*, and\n*logs*. **Sashiko is tracing-only.** It does not emit metrics or\nlogs, and it is not an analytics platform.\n\n✅ **Use Sashiko if you have:**\n\n- A Ruby backend (Rails / Sidekiq / Hanami / custom workers) using\n  OpenTelemetry, or planning to.\n- Code that spawns parallel work via `Thread` / `Fiber` / `parallel_map`\n  / `Ractor`, and you want the spans to stay connected to the parent\n  request.\n- Sidekiq / ActiveJob / Kafka / custom queue work where you want\n  the trace continuous across the queue boundary.\n- Ractor-based CPU-bound work that you want traced (vanilla OTel\n  raises `Ractor::IsolationError` when you try to emit spans from\n  inside a Ractor; Sashiko's span replay handles it).\n- Multi-tenant Ruby processes via `Ruby::Box` where each tenant\n  should get its own OTel pipeline.\n\n❌ **Don't use Sashiko for:**\n\n- **Web / mobile analytics** (page views, conversions, user\n  cohorts) — use Google Analytics, Mixpanel, Amplitude.\n  OpenTelemetry traces are not analytics events.\n- **Application metrics** (counters, gauges, histograms, real-time\n  dashboards) — use OpenTelemetry metrics + Prometheus / Grafana.\n- **Application logs** — use lograge / structured logging / OTel\n  logs. Sashiko does not handle logs.\n- **Frameworks that already have first-party OTel support** (Rails,\n  Sidekiq, Faraday, gRPC, …) — pick up the corresponding\n  `opentelemetry-instrumentation-*` SIG gem instead. Use Sashiko\n  for *your own code* that lives next to those frameworks.\n\nThe shortest discriminator: if your question is **\"why did this\nparticular request take 3 seconds and where was the time spent\"**,\nSashiko is in scope. If your question is **\"how many users converted\nlast week\"** or **\"what is our p99 latency over time\"**, it isn't.\n\n## Requirements\n\n- Ruby 4.0 or later (4.0.0 shipped 2025-12-25)\n- `opentelemetry-api` `~\u003e 1.4`\n- `opentelemetry-sdk` `~\u003e 1.5`\n\n`Ruby::Box` and `Ractor::Port`-backed features need Ruby 4.0+. Box is\nopt-in via `RUBY_BOX=1` and is flagged experimental upstream — see\n[Misc #21681](https://bugs.ruby-lang.org/issues/21681).\nThe Thread, Fiber, queue, and HTTP boundary helpers work on any Ruby\n3.x tested release, but the gem currently targets 4.0+ only because\nadapters use `Data.define` and other 3.2+ idioms.\n\n## Quick start\n\n```ruby\n# Gemfile\ngem \"sashiko\", \"~\u003e 0.1\"\ngem \"opentelemetry-sdk\"\ngem \"opentelemetry-exporter-otlp\"\n```\n\n(While `0.x.x`, expect the API to change. Pin to a patch version if\nyou want zero surprises. Pre-1.0 follows Keep-a-Changelog style — see\n[`CHANGELOG.md`](CHANGELOG.md) for migration notes between releases.)\n\n```ruby\n# config/initializers/otel.rb\nrequire \"opentelemetry/sdk\"\nrequire \"opentelemetry/exporter/otlp\"\n\nOpenTelemetry::SDK.configure { it.service_name = \"my-app\" }\n\nrequire \"sashiko\"\n```\n\n```ruby\nclass OrderService\n  extend Sashiko::Traced\n\n  trace :checkout, attributes: -\u003e(order) { { \"order.id\" =\u003e order.id } }\n  def checkout(order)\n    charge(order)\n    notify(order)\n  end\n\n  trace :charge\n  trace :notify\n  def charge(order); ...; end\n  def notify(order); ...; end\nend\n```\n\nEvery call to `checkout` produces a span named `OrderService#checkout`,\nwith `charge` and `notify` as children. Exceptions are recorded and the\nspan is marked errored automatically.\n\n## Core API\n\n### `Sashiko::Traced` — declarative spans\n\n```ruby\nclass Svc\n  extend Sashiko::Traced\n\n  # Wrap a single method.\n  trace :work, attributes: -\u003e(x) { { \"work.id\" =\u003e x.id } }\n\n  # Wrap every method matching a pattern (declare AFTER the defs).\n  trace_all matching: /^handle_/\nend\n```\n\n`trace` options:\n- `name:` — override the span name (defaults to `ClassName#method`).\n- `kind:` — `:internal` (default), `:client`, `:server`, etc.\n- `attributes:` — a `Proc` receiving call args, or a static `Hash`.\n- `record_args: true` — include arg count as `code.args.count`.\n\nImplementation: one anonymous module is `prepend`ed per target class at\ntrace time; subsequent `trace` calls redefine methods on the same overlay,\nkeeping `super` chains intact.\n\n### `Sashiko::Context` — propagation across Thread / Fiber\n\n```ruby\n# Thread.new that preserves the current OTel Context.\nSashiko::Context.thread { do_work }.join\n\n# Fiber.new likewise.\nSashiko::Context.fiber { do_work }.resume\n\n# Fan-out helper: one thread per item, all with the captured Context,\n# results returned in input order.\nresults = Sashiko::Context.parallel_map(jobs) { |j| process(j) }\n```\n\nWithout these helpers, plain `Thread.new` drops the OTel Context and\nyour spans become orphans.\n\n### `Sashiko::Context.carrier` — across processes, queues, Ractors\n\nThe same primitive works for any boundary where you can pass strings:\n\n```ruby\n# Producer side: capture the current trace context as a serializable Hash.\nqueue.push(payload: \"...\", trace_context: Sashiko::Context.carrier)\n\n# Worker side: re-attach it before doing traced work.\njob = queue.pop\nSashiko::Context.attach(job[:trace_context]) do\n  process(job)\nend\n```\n\n`carrier` is a deep-frozen, `Ractor`-shareable Hash of W3C Trace Context\nheaders (`traceparent`, `tracestate`). It survives JSON serialization,\nSidekiq job args, Kafka message attributes, HTTP headers, and\n`Ractor.new(...)` arguments.\n\n![Carrier propagation across Web → Sidekiq → external API](docs/assets/carrier_propagation.svg)\n\n### `Sashiko::Ractor` — parallel execution with span replay\n\nFor CPU-bound work that should actually use multiple cores:\n\n```ruby\nmodule PrimePipeline\n  def self.run(upper_bound)\n    candidates = Sashiko::Ractor.span(\"enumerate\") { (2..upper_bound).to_a }\n    primes     = Sashiko::Ractor.span(\"sieve\")     { candidates.select { |i| (2..Math.sqrt(i)).none? { |d| i % d == 0 } } }\n    Sashiko::Ractor.span(\"summarize\", attributes: { \"prime.count\" =\u003e primes.length }) { primes.last }\n  end\nend\n\nSashiko.tracer.in_span(\"main.batch\") do\n  Sashiko::Ractor.parallel_map([5_000, 10_000, 15_000], via: PrimePipeline.method(:run))\nend\n```\n\nHow it works:\n\n1. Each item runs in its own Ractor (true parallelism, no GVL).\n2. Inside each Ractor, `Sashiko::Ractor.span(...)` records a frozen\n   `SpanEvent` (name, start/end ns, attributes, parent event id) — *no\n   OTel calls, because those don't work inside a Ractor*.\n3. When the Ractor finishes, the batch of events is sent back via\n   `Ractor::Port` to the main Ractor.\n4. A `Sashiko::Ractor::Sink` on the main side calls\n   `tracer.start_span(..., start_timestamp: …)` /\n   `span.finish(end_timestamp: …)` for each event, rebuilding the parent\n   chain from the recorded event ids, all under the context of the span\n   that wrapped `parallel_map`.\n\nResulting tree:\n\n```\nmain.batch (20ms)                              ← main Ractor\n├─ PrimePipeline.run (7.5ms) [item.index=0]    ← recorded inside a Ractor\n│  ├─ enumerate                                ← ← nested Ractor-side span\n│  ├─ sieve\n│  └─ summarize\n├─ PrimePipeline.run (13.5ms) [item.index=1]\n│  └─ ...\n└─ PrimePipeline.run (19.7ms) [item.index=2]\n   └─ ...\n```\n\n![Ractor span replay sequence](docs/assets/ractor_span_replay.svg)\n\n```sh\nbundle exec ruby examples/ractor_span_replay_demo.rb\n```\n\n**Caveats — what \"replay\" does and doesn't preserve:**\n\n- `trace_id` / `span_id` are assigned by the **main-side** tracer at\n  replay time; the Ractor never sees a real `SpanContext`. Parent linkage\n  inside the replayed batch is correct, and the batch's root attaches to\n  whatever main-side context wrapped `parallel_map`.\n- `OpenTelemetry::Baggage` set inside the Ractor is **not** propagated\n  out; only what's in `Sashiko::Context.carrier` at `parallel_map` time\n  reaches the replay.\n- Sampling is decided at replay time on the main side, not when the work\n  actually ran.\n- Constraints: `via:` must be a `Method` whose receiver is\n  Ractor-shareable (a `Module` or frozen class). Nested spans inside the\n  Ractor must use `Sashiko::Ractor.span` — `tracer.in_span` would crash.\n\nFor threads, prefer `Sashiko::Context.thread` / `parallel_map` — Ractor\nreplay is for genuine multi-core CPU work.\n\n### `Sashiko::Box` — per-Box instrumentation (Ruby 4, experimental)\n\nVanilla `Module#prepend`-based instrumentation is process-global: once\nyou patch `Anthropic::Messages` for tenant A, tenant B inherits the\npatch. `Ruby::Box` provides a separate loading namespace, so each Box\ncan have its own instrumented classes, OTel tracer provider, and exporter.\n\n```ruby\n# Run with: RUBY_BOX=1 bundle exec ruby your_script.rb\n\ntenant_a = Sashiko::Box.new\ntenant_a.eval(\u003c\u003c~RUBY)\n  OpenTelemetry::SDK.configure { |c| c.service_name = \"tenant-a\" }\n  # ... tenant-a's business code + instrumentation ...\nRUBY\n\ntenant_b = Sashiko::Box.new\ntenant_b.eval(\u003c\u003c~RUBY)\n  OpenTelemetry::SDK.configure { |c| c.service_name = \"tenant-b\" }\nRUBY\n```\n\n`Sashiko::Box.new` creates a `Ruby::Box` with Sashiko already required\ninside. It raises `NotEnabledError` if the process wasn't started with\n`RUBY_BOX=1`. For a bare box without Sashiko, use `Ruby::Box.new`\ndirectly.\n\n\u003e **Inside a Box, pass a `tracer:` explicitly.** Ruby::Box does isolate\n\u003e `OpenTelemetry.tracer_provider` (each Box has its own object). The\n\u003e reason `Sashiko.tracer` doesn't follow it is that the method is\n\u003e defined under a `respond_to?(:tracer)` guard at require time — when\n\u003e sashiko is re-required inside a Box, the guard skips redefinition,\n\u003e so the existing method body (whose constant resolution scope was\n\u003e main's) keeps returning main's tracer. The behavior is reproducible\n\u003e in [`examples/talk/06_box_otel_pollution.rb`](examples/talk/06_box_otel_pollution.rb).\n\u003e\n\u003e Every place Sashiko emits a span accepts an explicit `tracer:` that\n\u003e bypasses the default lookup:\n\u003e\n\u003e ```ruby\n\u003e tracer = OpenTelemetry.tracer_provider.tracer(\"my-component\")\n\u003e\n\u003e # Per-method\n\u003e trace :foo, tracer: tracer\n\u003e\n\u003e # Or every method\n\u003e trace_all matching: /^handle_/, tracer: tracer\n\u003e\n\u003e # Adapters\n\u003e f.use Sashiko::Adapters::Faraday::Middleware, tracer: tracer\n\u003e Sashiko::Adapters::Anthropic.instrument!(Anthropic::Messages, tracer: tracer)\n\u003e\n\u003e # Ractor\n\u003e Sashiko::Ractor.parallel_map(items, via: M.method(:run), tracer: tracer)\n\u003e ```\n\u003e\n\u003e `Sashiko::Adapters::Anthropic.instrument_in_box!` does this binding\n\u003e for you automatically when called with a Box.\n\n![Box multi-tenant isolation](docs/assets/box_multitenant.svg)\n\nFor instrumenting a specific class only inside a box:\n\n```ruby\nbox = Sashiko::Box.new\nbox.eval('require \"anthropic\"')\nSashiko::Adapters::Anthropic.instrument_in_box!(box, \"Anthropic::Messages\")\n```\n\nCaveats: `Ruby::Box` is experimental in Ruby 4.0. Known upstream issues\ninclude native-extension loading, `bundler/inline`, and parts of\n`active_support` failing under Box. See\n[Misc #21681](https://bugs.ruby-lang.org/issues/21681) for the roadmap.\n\nSee [`examples/box_multitenant_demo.rb`](examples/box_multitenant_demo.rb)\nfor a runnable demo.\n\n## Adapters\n\nAdapters are not loaded by default — require them explicitly. The core\ngem has zero vendor-specific code.\n\n### Rails\n\n```ruby\nrequire \"sashiko/rails\"\nSashiko::Rails.install!(notifications: /^my_app\\./)\n```\n\nRails companion that fills gaps left by SIG's\n`opentelemetry-instrumentation-rails`:\n\n- `Sashiko::Rails.async(\"name\") { ... }` — spawn a Thread that\n  preserves OTel context (orphans-be-gone for `Thread.new` in\n  controllers).\n- `include Sashiko::Rails::TracedJob` in `ApplicationJob` — ride the\n  trace carrier across any ActiveJob backend (Sidekiq, GoodJob,\n  SolidQueue, etc.).\n- `Sashiko::Rails.bridge_notifications(/regex/)` — turn matching\n  `ActiveSupport::Notifications` events into OTel spans.\n\nFull walkthrough: [`docs/rails_integration.md`](docs/rails_integration.md).\nNone of this monkey-patches Rails — pieces are independent and opt-in.\n\n### Faraday\n\n```ruby\nrequire \"sashiko/adapters/faraday\"\n\nconn = Faraday.new(\"https://api.example.com\") do |f|\n  f.use Sashiko::Adapters::Faraday::Middleware\nend\n```\n\nProduces client-kind spans named after the HTTP method (`GET`, `POST`,\netc.) per OTel HTTP semantic conventions, with the standard request /\nresponse attributes (`http.request.method`, `url.full`, `server.address`,\n`server.port`, `http.response.status_code`, `error.type`).\n\n### Anthropic (optional, may move to a separate gem)\n\n```ruby\nrequire \"sashiko/adapters/anthropic\"\n\nSashiko::Adapters::Anthropic.instrument!(Anthropic::Messages)\n```\n\nProduces GenAI semantic-convention spans on every `messages.create` call,\nincluding token counts, cache hit ratio, and an estimated USD cost.\nPricing is a frozen `Data.define(Price)` value, deep-frozen at load time;\noverride via `Sashiko::Adapters::Anthropic.pricing =`.\n\n\u003e The Anthropic adapter is **outside Sashiko's core \"concurrency-boundary\n\u003e observability\" scope** and is shipped here as a working reference.\n\u003e It may be extracted into a `sashiko-anthropic` gem in a future release;\n\u003e any move will be announced in the CHANGELOG with a deprecation notice.\n\u003e Model names, pricing, and the GenAI semantic conventions are still\n\u003e moving targets — treat this adapter as a convenience, not a stable\n\u003e contract.\n\n## Types\n\n```sh\nbundle exec rake typecheck\n```\n\n```\n..........\nNo type error detected. 🫖\n```\n\nIf you embed Sashiko in your own Steep project, add the sig path:\n\n```ruby\n# Steepfile\ntarget :app do\n  signature \"sig\"\n  signature \"vendor/bundle/ruby/4.0.0/gems/sashiko-*/sig\"\n  check \"app\"\nend\n```\n\n## Examples\n\nThe full list with one-line descriptions and a quick-reference shell\nblock is in [`examples/README.md`](examples/README.md). Highlights:\n\n- [`examples/thread_fanout_demo.rb`](examples/thread_fanout_demo.rb) —\n  before/after of `Thread.new` losing OTel context vs.\n  `Sashiko::Context.parallel_map` keeping it. *Start here.*\n- [`examples/queue_demo.rb`](examples/queue_demo.rb) — producer enqueues\n  jobs; workers continue the same distributed trace.\n- [`examples/ractor_span_replay_demo.rb`](examples/ractor_span_replay_demo.rb)\n  — each Ractor records nested spans; the main side reconstructs them as\n  one trace tree.\n- [`examples/box_multitenant_demo.rb`](examples/box_multitenant_demo.rb)\n  — two tenants share one Ruby process. Requires `RUBY_BOX=1`.\n\n## Development\n\n```sh\nbundle install\nbundle exec rake test    # run tests\nbundle exec rake docs    # generate RDoc to doc/\n```\n\n## Appendix: Ruby version targeting\n\nSashiko targets Ruby 4.0+ today. The boundary helpers\n(`Context.thread/fiber/parallel_map/carrier/attach`) and the\n`Traced` DSL would also work on Ruby 3.2+ — the floor is set by\ntwo specifically-4.0 capabilities and a handful of 3.x idioms used\nthroughout the codebase.\n\nRuby 4.0-specific:\n\n| Feature | Where in Sashiko |\n|---|---|\n| `Ractor::Port` | `Sashiko::Ractor.parallel_map` — Port-based result collection. |\n| `Ruby::Box` | `Sashiko::Box.new` and `Sashiko::Adapters::Anthropic.instrument_in_box!` — per-Box instrumentation. Tests run in both default and `RUBY_BOX=1` modes in CI. |\n\nRuby 3.x idioms applied where they help:\n\n| Feature | Where in Sashiko |\n|---|---|\n| `Data.define` (3.2+) | `Sashiko::Traced::Options`, `Adapters::Anthropic::Price`, `Ractor::SpanEvent` — frozen, Ractor-shareable values. |\n| Pattern matching (3.0+) | Attribute extraction in `Traced`, HTTP status classification in Faraday adapter, GenAI response disjunction in Anthropic adapter. |\n| `Ractor.make_shareable` (3.0+) | `Sashiko::Context.carrier` returns a deep-frozen, Ractor-shareable Hash. `DEFAULT_PRICING` is shareable too. |\n| Endless methods (3.0+), anonymous block forwarding (3.1+) | Used in `lib/` where they remove a line without obscuring intent. |\n| `it` block param (3.4+) | Used in tests and examples; `lib/` keeps named block params for now since Steep does not yet recognize `it`. |\n| RBS + Steep | `sig/sashiko.rbs`, type-checked in CI. |\n\nClass-load-time `Module#prepend` is the only mechanism used for\ninstrumentation; no `method_added` hooks, no runtime `define_method` at\ncall time. Inline caches stay warm.\n\n## License\n\nMIT.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fo6lvl4%2Fsashiko","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fo6lvl4%2Fsashiko","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fo6lvl4%2Fsashiko/lists"}