Natchez Distributed Tracing for Scala

repository·main·Indexed 43 days ago

https://github.com/typelevel/natchez

A distributed tracing library for Scala designed to instrument applications and observe requests across distributed systems. It provides a core API for managing spans via EntryPoint and Span traits, and includes back-ends for OpenTelemetry, OpenCensus, and structured JSON log output. It supports standardized trace tags for HTTP, database, and peer metadata, and integrates with cats.effect.Resource for lifecycle management of exporters.

Tokens
18.9K
Snippets
58
Records
75
Agent score
56%

What's inside Natchez

  1. Use the Log Handler for JSON trace output

    main

    The Log Handler is a Natchez back-end designed to print traces to a logger in a structured JSON format.

    Key characteristics:

    • JSON Structure: Spans are emitted as JSON objects where span fields are represented as object properties.
    • Hierarchy: A special children property is included in each JSON object to contain a list of its child spans, allowing for a nested representation of the trace.
    • Tracing Support: While continued traces maintain correct parent span and trace IDs, full distributed tracing is not supported because this handler does not include a span collector.
  2. Supported tracing back ends in Natchez

    main

    Natchez provides support for various tracing back ends to export your telemetry data. If you are unsure which back end to choose, Jaeger is recommended as a starting point because it is easy to set up using Docker.

    Supported back ends:
    * Datadog
    * Honeycomb
    * Jaeger
    * Lightstep
    * Log
    * Mock
    * New Relic
    * No-Op
    * Odin
    * OpenCensus
    * OpenTelemetry
  3. What is a Span in Natchez?

    main

    A Span is a managed resource that records timing information and metadata about a specific computation. Spans are organized into a tree structure: a Span can create child spans, allowing you to capture different phases of a larger computation.

    Key characteristics:

    • Hierarchical: Spans form a tree where child lifetimes are typically bounded by the parent's lifetime.
    • Metadata: Each span contains a map of TraceValues (strings, booleans, and numbers) for user-defined information.
    • Distributed: Spans can provide a Kernel (a hunk of data) that can be sent to remote computers (e.g., via HTTP headers) to link traces across different machines.
  4. Use the Trace constraint for ambient span access

    main

    In modern Scala applications (especially those using Cats-Effect 3 or tagless-final style), you should use the Trace constraint rather than passing Span objects explicitly.

    If your effect type F[_] has a Trace constraint (F[_]: Trace), you can perform tracing operations without direct reference to a Span instance. This allows you to:

    • Add fields to the ambient span.
    • Retrieve the current span's Kernel.
    • Run a computation within a child span of the ambient span.
  5. Understand the role of an EntryPoint

    main
    An EntryPoint is the starting point for using Natchez. It provides the mechanism to either create a new root Span or to continue a Span that originated on a different machine. In a production application, you typically construct an EntryPoint via a vendor-specific factory (e.g., for Jaeger, Zipkin, etc.) during IOApp.run and use that single instance for the lifetime of your program. It represents a live connection to your tracing back-end.
  6. Use the Trace constraint for tagless style tracing

    main

    In tagless style applications, instead of passing a Span explicitly, you can use the Trace[F] constraint on your abstract effect F. This allows you to add fields to the ambient span, access its kernel, or run computations within a child span using Trace[F].span and Trace[F].put.

    To use this, ensure your effect F has a Trace instance available in scope.

    import cats.{ Applicative, Monad }
    import cats.data.Kleisli
    import cats.effect.MonadCancel
    import cats.mtl.Local
    import cats.syntax.all._
    import natchez.{ Span, Trace }
    import natchez.mtl._
    
    // Example of a traced function using the Trace constraint
    def wibble[F[_]: Monad: Trace](name: String, age: Int): F[Unit] =
      Trace[F].span("wibble") {
        for {
          _ <- Trace[F].put("name" -> name, "age" -> age)
          // ... actual method logic in F
        } yield ()
      }
  7. How to construct Spans

    main

    Spans are constructed in three primary ways depending on their role in the trace:

    Child Spans

    Most spans are created from an existing Span or Trace instance using the span method.

    • Explicit: Returns a Resource that you must .use.
    • Ambient: Takes a continuation block to execute within the context of the child span.

    Root Spans

    Constructed via an EntryPoint when there is no parent span (e.g., when a new external web request is received).

    Continued Spans

    Constructed via an EntryPoint when the parent span exists on a different computer (e.g., when a new internal request is received via a distributed trace).

  8. What are Kernels and how do they enable distributed tracing?

    main

    A Kernel is a back-end-specific hunk of data associated with a Span. It contains the necessary state to allow a trace to continue on a different computer or process. By passing a Kernel from one service to another, tracing becomes distributed.

    Technically, a Kernel wraps a Map[String, String] (accessible via toHeaders), which is designed to be easily serialized into transport formats like HTTP headers. On the receiving end, the remote service parses these headers to reconstruct the span context and continue the trace.

  9. Understand the core abstractions: Spans, EntryPoints, and Traces

    main

    Natchez abstracts over distributed tracing back-ends using three primary concepts:

    1. Spans: The central unit of work. A Span is a managed resource that records timing and metadata. Spans form a tree structure where a span can create child spans to represent sub-phases of a computation. Spans also support arbitrary user-defined metadata via a map of TraceValues (strings, booleans, and numbers).
    2. EntryPoints: The mechanism used to obtain the first span in a process. An EntryPoint can create a root span (a new trace) or a continued span (linking to a parent span on a different machine). To continue a trace across a network, you must provide the Kernel from the parent span (e.g., via HTTP headers).
    3. Trace Constraint: A type class constraint used in tagless-final or Cats-Effect 3 applications to provide an ambient span. Instead of passing a Span object manually through every function, an effect F[_] with a Trace constraint allows you to add fields, retrieve kernels, or create child spans using the ambient context.
  10. How to add fields to a Span

    main

    Spans contain an internal map of string-keyed tags and TraceValues (strings, booleans, or numbers) called fields.

    • Adding fields: Use the put method on either a Span (explicit) or Trace (ambient) instance.
    • Standard Tags: You can use the Tags object to construct fields that follow emerging standards like OpenTracing or OpenTelemetry. Many back-ends recognize these.
    • Custom Tags: You can use arbitrary strings as field tags for user-defined metadata.
    • Note: Fields are write-only. You cannot inspect the fields of a span within your code.
  11. Use Spans for distributed tracing with Kernels

    main
    To implement distributed tracing, you can use a Span to generate a Kernel. A Kernel is a hunk of data that can be sent to a remote computer (typically via HTTP headers). When the remote computer receives this kernel, it can use an EntryPoint to create a continued span that is linked to the originating span by the tracing back-end, allowing the trace to span multiple machines.
  12. Satisfy Trace constraint using Cats-MTL Local

    main

    By adding the natchez-mtl module, you can satisfy the Trace[F] constraint if you have MonadCancel[F, Throwable] and Local[F, Span[F]]. This approach is more general than the Kleisli method and allows for instantiating F as types like RWST.

    // Requires natchez-mtl and Cats-MTL
    def goLocal[F[_]](
      implicit ev0: MonadCancel[F, Throwable],
               ev1: Local[F, Span[F]]
    ): F[Unit] =
      wibble("bob", 42)