> For the complete documentation index, see [llms.txt](/llms.txt)

# Telemetry hooks

Every generated client accepts an optional set of **telemetry hooks** — callbacks fired around
each request so you can log, emit metrics, or open a trace span without wrapping the client. They
are off unless you supply them, and add no overhead when absent.

**TypeScript** (regenerated + byte-diffed in CI `a0f8af4b462c`)

```ts
import { Client } from 'glotto-sdk';

const client = new Client({
  hooks: {
    onRequest: ({ method, url }) => console.log('request', method, url),
    onResponse: ({ status, durationMs }) => console.log('response', status, durationMs),
  },
});
```

**React Native** (regenerated + byte-diffed in CI `a1fefe7d3458`)

```ts
import { Client } from 'glotto-sdk';

const client = new Client({
  hooks: {
    onRequest: ({ method, url }) => console.log('request', method, url),
    onResponse: ({ status, durationMs }) => console.log('response', status, durationMs),
  },
});
```

**Python** (regenerated + byte-diffed in CI `2242160f8958`)

```python
from glotto_sdk import Client, TelemetryHooks

hooks = TelemetryHooks(
    on_request=lambda ctx: print(ctx.method, ctx.url),
    on_response=lambda ctx: print(ctx.status, ctx.duration_ms),
)
client = Client(hooks=hooks)
```

**Go** (regenerated + byte-diffed in CI `f0c565cc46f8`)

```go
package main

import (
    "log"

    sdk "example.com/glotto-sdk-go"
)

func main() {
    client := sdk.NewClient(sdk.WithHooks(&sdk.TelemetryHooks{
        OnRequest: func(ctx sdk.TelemetryContext) {
            log.Printf("%s %s", ctx.Method, ctx.URL)
        },
        OnResponse: func(ctx sdk.TelemetryContext) {
            log.Printf("%d %.0fms", ctx.Status, ctx.DurationMs)
        },
    }))
    _ = client
}
```

**Java** (regenerated + byte-diffed in CI `12c05e098737`)

```java
import com.glotto.Client;
import com.glotto.TelemetryHooks;

public class Snippet {
    public static void main(String[] args) {
        Client client = Client.builder().token("<token>").build();
        client.setHooks(new TelemetryHooks(
            ctx -> System.out.println(ctx.method() + " " + ctx.url()),
            ctx -> System.out.println(ctx.status() + " " + ctx.durationMs()),
            null,
            null
        ));
    }
}
```

**Kotlin** (regenerated + byte-diffed in CI `0f5e53ae2522`)

```kotlin
import com.glotto.Client
import com.glotto.TelemetryHooks

fun main() {
    val hooks = TelemetryHooks(
        onRequest = { ctx -> println("${ctx.method} ${ctx.url}") },
        onResponse = { ctx -> println("${ctx.status} ${ctx.durationMs}") },
    )
    val client = Client(token = "<token>", hooks = hooks)
    println(client)
}
```

**C#** (regenerated + byte-diffed in CI `c5013ce0e88b`)

```csharp
using Glotto;

var hooks = new TelemetryHooks(
    OnRequest: ctx => Console.WriteLine($"{ctx.Method} {ctx.URL}"),
    OnResponse: ctx => Console.WriteLine($"{ctx.Status} {ctx.DurationMs}"));

var client = new Client(new GlottoClientOptions { Hooks = hooks });
```

**PHP** (regenerated + byte-diffed in CI `1acd16081b46`)

```php
<?php
require 'vendor/autoload.php';

$hooks = new Glotto\Core\TelemetryHooks(
    onRequest: fn ($ctx) => print($ctx->method . " " . $ctx->url . PHP_EOL),
    onResponse: fn ($ctx) => print($ctx->status . " " . $ctx->duration_ms . PHP_EOL),
);
$client = new Glotto\Client(token: '<token>', hooks: $hooks);
```

**Ruby** (regenerated + byte-diffed in CI `a2b854f6593f`)

```ruby
require 'glotto'

hooks = Glotto::TelemetryHooks.build(
  on_request: ->(ctx) { puts "#{ctx.method} #{ctx.url}" },
  on_response: ->(ctx) { puts "#{ctx.status} #{ctx.duration_ms}" }
)
client = Glotto::Client.new(token: '<token>', hooks: hooks)
```

**Rust** (regenerated + byte-diffed in CI `716e9ec181d1`)

```rust
let hooks = TelemetryHooks {
    on_request: Some(Box::new(|ctx| println!("{} {}", ctx.method, ctx.url))),
    on_response: Some(Box::new(|ctx| println!("{:?} {:?}", ctx.status, ctx.duration_ms))),
    ..TelemetryHooks::default()
};
let client = Client::default().with_token("<token>").with_hooks(hooks);
```

**Swift** (regenerated + byte-diffed in CI `0cc357352baf`)

```swift
import Foundation
import GlottoSdk

let hooks = TelemetryHooks(
    onRequest: { ctx in print(ctx.method, ctx.url) },
    onResponse: { ctx in print(ctx.status as Any, ctx.durationMs as Any) }
)
let client = Client(token: "<token>", hooks: hooks)
```

**Dart** (regenerated + byte-diffed in CI `67567d6df435`)

```dart
import 'package:glotto_sdk/glotto_sdk.dart';

final hooks = TelemetryHooks(
  onRequest: (ctx) => print('${ctx.method} ${ctx.url}'),
  onResponse: (ctx) => print('${ctx.status} ${ctx.durationMs}'),
);
final client = Client(token: "<token>", hooks: hooks);
```

**Elixir** (regenerated + byte-diffed in CI `b97af402f1d7`)

```elixir
hooks = %{
  on_request: fn ctx -> IO.inspect({ctx.method, ctx.url}) end,
  on_response: fn ctx -> IO.inspect({ctx.status, ctx.duration_ms}) end
}
client = Glotto.new(token: "<token>", hooks: hooks)
```

## The callbacks

The `hooks` object accepts `onRequest`, `onResponse`, `onError`, and `onRetry` (named idiomatically
per language — `onRequest`/`onResponse` in TypeScript, `on_request`/`on_response` in Python,
`OnRequest`/`OnResponse` in Go). Each receives a telemetry context with the request method, URL,
and — on the response side — the status and elapsed duration, so you can wire OpenTelemetry HTTP
client spans or your own metrics pipeline.

## Turning hooks off from the environment

Every generated client also reads **`OTEL_SDK_DISABLED`** from the process environment, once when
the client is constructed. When it is set to `true`, the hooks you passed are ignored and no
callback fires — so an operator can silence client-side telemetry in an environment without a code
change or a redeploy of the calling service.

```bash
OTEL_SDK_DISABLED=true ./your-service
```

**The value must be `true`, in any casing.** `true`, `TRUE` and `True` all disable telemetry, in
every one of the generated SDKs. **Every other value leaves telemetry enabled**, including `1`,
`yes`, `0`, `false`, an empty value, and the variable being unset.

That `1` does *not* disable telemetry is deliberate and worth stating plainly, because it is the
spelling most people try first. It is the rule
[OpenTelemetry's own specification](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/)
sets for this variable: it declares `OTEL_SDK_DISABLED` to be of the Boolean type, and defines that
type as true *only* for the case-insensitive string `true`, with implementations explicitly
forbidden from accepting anything wider. Following that rule is what makes the switch mean the same
thing in a Glotto SDK as it does in the rest of your OpenTelemetry stack — your language agent, your
auto-instrumentation, your collector — so one variable set once behaves the same everywhere. A
Glotto SDK that also honoured `1` would disable itself while the conformant implementations beside
it kept exporting, which is worse than not honouring it at all.

**A value the SDK cannot read is not silent.** Set it to anything that is neither `true` nor
`false` — `1`, `yes`, `0`, a typo — and the client writes one line to your language's warning
channel when it is constructed, naming the value it ignored:

```
glotto: OTEL_SDK_DISABLED is set to 1, which is not a boolean; telemetry stays enabled. Only true (any casing) disables it.
```

It goes to the warning sink, never to standard output, so it cannot corrupt a program whose stdout
carries a protocol: `console.warn` in TypeScript and React Native, `logging` in Python, standard
error in Go, Ruby, Java, Kotlin, C#, Rust, Swift and Dart, `error_log` in PHP, `IO.warn` in Elixir.
Nothing is written when the variable is unset, empty, `true` or `false` — a correct configuration
produces no output at all, so the line only ever appears while there is something to fix.

Two further things follow from the kill switch being an *environment* setting rather than a config
one, and both are deliberate:

- **It is not a `glotto.yml` key**, and you will not find it in your configuration. `glotto.yml`
  describes what gets generated; this decides what an already-generated client does at runtime, in
  a particular deployment. Baking it into the config would make silencing telemetry a regeneration.
- **It is read once, at construction.** Changing the variable in a running process does not affect
  a client that already exists — set it before the process starts.

## Hooks vs. telemetry headers

These callbacks are a **client-side observability seam** and are distinct from
`client_settings.telemetry_headers` — a separate, opt-in feature that makes every request *send*
the `X-Glotto-Retry-Count` and `X-Glotto-Timeout` headers so your **server** can see the client's
retry state and timeout budget. Hooks observe locally; telemetry headers tell the server. See the
[`glotto.yml` reference](/docs/glotto-yml) for `telemetry_headers`.
