PHPackages                             mesh0/sdk - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Logging &amp; Monitoring](/categories/logging)
4. /
5. mesh0/sdk

ActiveLibrary[Logging &amp; Monitoring](/categories/logging)

mesh0/sdk
=========

Official PHP SDK for the mesh0 AI telemetry platform — send logs, traces, and events; query with TQL.

1.3.0(2w ago)024↓50%MITPHPPHP ^8.2CI passing

Since May 11Pushed 2w agoCompare

[ Source](https://github.com/mesh0-ai/php-sdk)[ Packagist](https://packagist.org/packages/mesh0/sdk)[ Docs](https://mesh0.ai)[ RSS](/packages/mesh0-sdk/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (4)Dependencies (11)Versions (5)Used By (0)

mesh0 PHP SDK
=============

[](#mesh0-php-sdk)

[![CI](https://github.com/mesh0-ai/php-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/mesh0-ai/php-sdk/actions/workflows/ci.yml)

Official PHP client for the [mesh0](https://mesh0.ai) AI telemetry platform. Send logs, custom events, and OTLP traces; query them back with TQL.

- **PHP 8.2+** with strict types and readonly DTOs
- **PSR-3** logger you can drop into Laravel, Symfony, Slim, …
- **PSR-18** HTTP client — bring your own (Guzzle, Symfony HTTP, …) or rely on auto-discovery
- **Built-in retries** for transient failures with exponential backoff + jitter
- **Nested-span instrumentation** — `Mesh0\Trace\Tracer` for trees of operations (no-code blocks, request handlers, job pipelines)
- **Low-latency UDS-DGRAM path** — `~5µs/call` via the local mesh0 metrics-agent sidecar
- **Tested at PHPStan level 9**

---

Installation
------------

[](#installation)

```
composer require mesh0/sdk
```

If you don't already have a PSR-18 client installed, add Guzzle:

```
composer require guzzlehttp/guzzle
```

---

Quick start
-----------

[](#quick-start)

```
use Mesh0\Client;
use Mesh0\Event\Event;

$mesh0 = Client::create('m0_abcde_xxxxxxxxxxxxxxxxxxxxxxxx');

// Send a single event. The wire shape is intentionally narrow — identity,
// time, plus two open bins (`attributes` queryable, `data` opaque).
// Anything domain-specific goes inside attributes / data.
$mesh0->events->send(
    Event::now()
        ->withAttributes([
            'app.id'          => 'checkout',
            'app.environment' => 'prod',
            'span.name'       => 'charge.captured',
            'user.id'         => 'user_42',
            'order_id'        => 'ord_123',
            'amount_usd'      => 19.99,
        ]),
);
```

Or load configuration from the environment (`MESH0_API_KEY`, `MESH0_BASE_URL`):

```
$mesh0 = Client::fromEnv();
```

---

Sending logs (PSR-3)
--------------------

[](#sending-logs-psr-3)

The fastest way to start streaming telemetry to mesh0 is the bundled PSR-3 logger. Plug it into any framework that takes a `Psr\Log\LoggerInterface`:

```
$logger = $mesh0->logger(defaults: [
    'app.id'          => 'web',
    'app.environment' => 'prod',
]);

$logger->info('user {user} signed up', ['user' => 'alice', 'plan' => 'pro']);

try {
    chargeCard($order);
} catch (\Throwable $e) {
    $logger->error('charge failed', [
        'exception' => $e,
        'order_id'  => $order->id,
        'user.id'   => $order->userId,
    ]);
}
```

Context keys that map to wire-level event fields are lifted out; everything else is merged into `attributes`:

Context keyLifted to top-level wire field`event_id``event_id``trace_id``trace_id``span_id``span_id``parent_span_id``parent_span_id`Plus: `exception` (Throwable) writes `error.type` and `error.message`into `attributes`. The interpolated message and `log.level` always land in `attributes`. `status` and `duration_ms` are no longer special — pass them as ordinary context keys (`'status' => 'error'`, `'duration_ms' => 142`) and they land in `attributes` like everything else. Records are buffered in memory and flushed on `flush()`, when the buffer fills, and on shutdown.

If you pass a [`Tracer`](#instrumenting-nested-operations-tracer) to `logger(...)`, log records emitted inside an active span pick up `trace_id` / `span_id` automatically when you don't supply them yourself.

The logger never throws — delivery failures are swallowed so your request path stays alive. Pass an optional `fallback` PSR-3 logger if you want visibility into why telemetry vanished:

```
$logger = $mesh0->logger(
    defaults: ['app.id' => 'web'],
    fallback: $appLogger, // receives flush errors + malformed-input warnings
);
```

### Laravel

[](#laravel)

```
// config/logging.php
'channels' => [
    'mesh0' => [
        'driver' => 'custom',
        'via'    => fn () => Mesh0\Client::fromEnv()->logger(defaults: [
            'app.id'          => config('app.name'),
            'app.environment' => config('app.env'),
        ]),
    ],
],
```

### Symfony / Monolog

[](#symfony--monolog)

Add a `psr` handler pointing at the mesh0 logger service:

```
# config/services.yaml
services:
    Mesh0\Client:
        factory: ['Mesh0\Client', 'fromEnv']
    Psr\Log\LoggerInterface $mesh0Logger:
        factory: ['@Mesh0\Client', 'logger']
        # Pass defaults via the constructor's $defaults argument
        arguments:
            $defaults:
                app.id: '%env(APP_NAME)%'
                app.environment: '%kernel.environment%'
```

---

Sending events directly
-----------------------

[](#sending-events-directly)

The `Event` builder is fluent and immutable — every `with*` call returns a new builder.

```
$mesh0->events->send(
    Event::now()
        ->withTraceId($traceId)
        ->withAttributes([
            'app.id'                       => 'agents',
            'app.environment'              => 'prod',
            'span.name'                    => 'agent.run',
            'duration_ms'                  => 820,
            'status'                       => 'success',
            'gen_ai.system'                => 'anthropic',
            'gen_ai.request.model'         => 'claude-opus-4-7',
            'gen_ai.usage.input_tokens'    => 1_240,
            'gen_ai.usage.output_tokens'   => 380,
            'gen_ai.usage.cost_usd'        => 0.0184,
            'tools'                        => ['search', 'retrieve'],
            'workflow'                     => 'onboarding',
        ])
        // Big payloads (LLM message arrays, raw req/resp) go in `data` —
        // opaque, not TQL-queryable, only shown on single-event drilldown.
        ->withData(['messages' => $messages]),
);

// Bulk: send up to 5,000 events per HTTP call. Larger batches are split.
$mesh0->events->sendMany($events);
```

---

OTLP traces
-----------

[](#otlp-traces)

mesh0 accepts OTLP/HTTP JSON at `/v1/traces`. Point any OpenTelemetry exporter at it with the same Bearer token:

```
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.mesh0.ai
OTEL_EXPORTER_OTLP_PROTOCOL=http/json
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer m0_abcde_xxxxxxxxxxxxxxxxxxxxxxxx
```

The SDK exposes the read side:

```
$spans = $mesh0->traces->get($traceId);
```

---

Metrics (statsd / DogStatsD over UDS-DGRAM)
-------------------------------------------

[](#metrics-statsd--dogstatsd-over-uds-dgram)

For high-frequency counters, gauges, and timings — the kind of telemetry that shouldn't go through the request-blocking HTTPS path — point at a co-located [mesh0 metrics-agent](https://github.com/mesh0-ai/metrics-agent)sidecar over its Unix datagram socket. UDP support was removed in 1.0; the SDK speaks `udg://` exclusively.

Set the agent's bind path once via env or `Config`:

```
export MESH0_AGENT_SOCKET=/run/mesh0/agent.sock
```

```
$metrics = $mesh0->metrics(); // reads MESH0_AGENT_SOCKET / Config::$agentSocketPath

$metrics->increment('checkout.charge', tags: ['tier' => 'pro']);
$metrics->gauge('queue.depth', 42);
$metrics->timing('db.query_ms', 12.4, tags: ['table' => 'orders']);
$metrics->histogram('upload.bytes', 8192);

// Convenience: time a block; metric is emitted whether $fn returns or throws.
$rows = $metrics->time('db.select_ms', fn () => $pdo->query($sql)->fetchAll());
```

The socket is opened lazily on the first send, so `$mesh0->metrics()`does no I/O. Per-call override:

```
$metrics = $mesh0->metrics(socketPath: '/tmp/mesh0-test.sock', defaultTags: [
    'service' => 'checkout',
    'env'     => 'prod',
]);
```

The agent must be configured with a matching `MESH0_LISTEN_ADDR`(`unix:///run/mesh0/agent.sock`). Calling `metrics()` (or `events()->agent()`) without an `agentSocketPath` set throws `ConfigurationException` — there is no UDP loopback fallback.

### Failure semantics

[](#failure-semantics)

Datagram send failures (peer unreachable, agent not running) are swallowed — the request path never throws on transport. Pass an optional PSR-3 logger via `new AgentMetricSink($path, $log)` to surface a single warning per state transition (open failure, write failure, oversize drop). The open-failure latch is terminal for the lifetime of the sink — long-lived workers that need to recover from a transient agent restart should construct a fresh sink rather than rely on auto-reopen. Malformed metric names or tags throw `ConfigurationException` so programmer errors fail loudly in development rather than silently disappearing. `sampleRate` outside `(0, 1]` is clamped (≤0 drops, ≥1 always emits) rather than throwing.

---

Sending events over UDS-DGRAM (low-latency)
-------------------------------------------

[](#sending-events-over-uds-dgram-low-latency)

For short-lived processes (PHP request handlers, CLI workers) that can't afford an HTTPS roundtrip per event, fire events at the same metrics-agent sidecar as JSON datagrams (~5µs per call):

```
$agent = $mesh0->events->agent(); // reads MESH0_AGENT_SOCKET / Config::$agentSocketPath

$agent->send(
    Mesh0\Event\Event::now()
        ->withAttributes([
            'app.id'          => 'checkout',
            'app.environment' => 'prod',
            'span.name'       => 'charge.succeeded',
            'order_id'        => 'ord_123',
        ]),
);

// Bulk loop — the agent batches before forwarding to mesh0.
$agent->sendMany([$e1, $e2, $e3]);
```

The socket is opened lazily on the first send. Datagrams larger than 32KB are dropped with a single warning (pass a PSR-3 logger to observe), and transport errors are swallowed — `send()` never throws.

This path is **at-most-once**: if the local agent is down or the kernel drops the datagram, the event is gone. For at-least-once durability, use `$mesh0->events->send(...)` which POSTs to `/v1/events` directly.

---

Instrumenting nested operations (Tracer)
----------------------------------------

[](#instrumenting-nested-operations-tracer)

For trees of nested operations — no-code block executions, request → job pipelines, anything where a parent's wall-clock includes its children — use `Mesh0\Trace\Tracer`. It manages a per-execution `trace_id` and a stack of `span_id`s, and emits exactly one event per closed span through any `EventSink` (typically the same agent sink shown above):

```
$tracer = $mesh0->tracer();

// Closure form — exception-safe, auto-pop, recommended:
$result = $tracer->span(['span.name' => 'block.if', 'block_id' => 'b_123'], function () use ($tracer) {
    return $tracer->span(['span.name' => 'block.http_request', 'url' => $url], fn () => $client->get($url));
});

// Manual form — when a closure doesn't fit (e.g. block dispatchers):
$h = $tracer->enter(['span.name' => 'block.loop', 'block_id' => 'b_456']);
try {
    // run block...
    $tracer->exit($h, attributes: ['iterations' => $n]);
} catch (\Throwable $e) {
    $tracer->exit($h, [
        'status'        => 'error',
        'error.type'    => $e::class,
        'error.message' => $e->getMessage(),
    ]);
    throw $e;
}
```

The Tracer never injects attribute keys for you. By convention (per the mesh0 data model) callers set `attributes["span.name"]` and, on the error path, `attributes["status"]` / `attributes["error.type"]` / `attributes["error.message"]` — these are normal attribute keys and the closure form of `span()` leaves them entirely to you. The Tracer also no longer auto-stamps a duration; if you want span wall time to be queryable, write it to `attributes["duration_ms"]` yourself before exit (or measure it in the manual form and pass it through).

Each `enter`/`exit` pair becomes one independent datagram on the way out; the metrics-agent forwards them verbatim and ClickHouse reassembles the trace via `trace_id` at query time. There is no "session start" or "session end" — children always close before parents because the parent's frame is still on the stack while children run.

**Long-lived workers (FrankenPHP, RoadRunner, Swoole)** must call `$tracer->reset()` between requests so trace state doesn't leak across them. A non-empty stack at reset time logs a warning through the PSR-3 logger you pass to the constructor.

**Adopting an incoming trace** (W3C `traceparent` header):

```
$tracer->startTrace($_SERVER['HTTP_TRACEPARENT'] ?? null);
// First enter() of the request now links to the upstream parent span.
```

**Logs that auto-correlate to the active span:** pass the tracer when building the logger and any record emitted inside a `span()` will pick up `trace_id` / `span_id` automatically when not supplied in the PSR-3 context:

```
$logger = $mesh0->logger(
    defaults: ['app.id' => 'no-code-runtime'],
    tracer: $tracer,
);

$tracer->span(['span.name' => 'block.http_request'], function () use ($logger) {
    $logger->info('calling upstream'); // trace_id / span_id stamped automatically
});
```

---

Querying
--------

[](#querying)

```
// Only the identity/time TQL builtins resolve at the top level:
// `timestamp, project.id, trace.id, span.id, parent_span.id`. Anything
// else (status, duration_ms, span.name, gen_ai.*, …) must be exposed via
// a per-project alias or promoted column — set those up in the dashboard,
// then reference them by their alias name here.
$rows = $mesh0->query->run([
    'from'    => 'events',
    'select'  => ['status', 'count()'],
    'where'   => ['status' => 'error'],
    'groupBy' => ['status'],
    'orderBy' => [['count()', 'desc']],
    'limit'   => 25,
]);
```

Pagination is also available on the events resource:

```
$page = $mesh0->events->list(limit: 100);
foreach ($page['events'] as $row) { /* … */ }

// Or stream every event, transparently following cursors:
foreach ($mesh0->events->iterate() as $row) { /* … */ }
```

---

Control-plane resources
-----------------------

[](#control-plane-resources)

Thin wrappers over the project- and user-scoped admin endpoints. Payloads are passed through as assoc arrays — see the backend route or each method's PHPDoc for accepted fields.

```
// Alerts (project key, m0_… — POST sends Idempotency-Key automatically).
$alerts   = $mesh0->alerts->listAlerts();
$channels = $mesh0->alerts->listChannels();
$mesh0->alerts->createAlert([/* AlertInput */]);

// PII scrubbers (project key, requires pii:read / pii:write scopes).
$rules = $mesh0->piiScrubbers->listScrubbers();
$mesh0->piiScrubbers->createScrubber([
    'name' => 'Credit cards', 'slug' => 'cc', 'kind' => 'regex',
    'pattern' => '\d{13,19}', 'replacement' => '[CC]', 'scope' => ['data'],
]);
$mesh0->piiScrubbers->setMode('enforce'); // 'enforce' | 'audit' | 'off'

// Schema — aliases + promoted (typed) columns
// (project key, requires schema:read / schema:write scopes).
$mesh0->schema->createAlias('order_id', 'order.id', 'string');
$mesh0->schema->promoteAlias('order_id'); // 202, status: "materializing"
$mesh0->schema->cancelPromotion('order_id'); // KILL+DROP while materializing
$mesh0->schema->demotePromoted('order_id');  // CH column retained for forensics

// Usage read endpoints (project key, requires usage:read scope).
$summary = $mesh0->usage->summary();         // current cap period
$series  = $mesh0->usage->series('2026-01-01', '2026-04-01', 'month');

// User / org / project management (user key, m0u_…).
$me    = $mesh0->user->me();
$keys  = $mesh0->user->listProjectKeys('acme', 'p_1');
```

POSTs that create resources without server-side `Idempotency-Key`middleware (`createScrubber`, schema `createAlias` / `promoteAlias` / `cancelPromotion`, `/v1/user/*` creates) are *not* retried — a transient 5xx would otherwise risk minting a duplicate or replaying a destructive KILL+DROP. PATCH / PUT / DELETE retain the default retry policy.

---

Configuration
-------------

[](#configuration)

```
use Mesh0\Client;
use Mesh0\Config;

$mesh0 = new Client(new Config(
    apiKey: 'm0_abcde_xxxxxxxxxxxxxxxxxxxxxxxx',
    baseUrl: 'https://api.mesh0.ai',
    timeout: 10.0,
    connectTimeout: 5.0,
    maxRetries: 2,
    userAgent: 'my-app/1.0',
    defaultHeaders: ['X-Tenant' => 'acme'],
));
```

### Environment variables

[](#environment-variables)

VariableDescription`MESH0_API_KEY`API key (`m0__`). **Required.**`MESH0_BASE_URL`Override base URL (self-hosted deployments).`MESH0_AGENT_SOCKET`Absolute path to the metrics-agent's Unix datagram socket. Required for `metrics()` / `events->agent()`.### Custom HTTP client

[](#custom-http-client)

`Client` accepts any PSR-18 client. Bring your own to share connection pooling, plug in middleware, or run against a fake in tests:

```
use GuzzleHttp\Client as Guzzle;
use GuzzleHttp\Psr7\HttpFactory;

$guzzle = new Guzzle(['timeout' => 5]);
$factory = new HttpFactory();

$mesh0 = new Client(Config::fromEnv(), $guzzle, $factory, $factory);
```

---

Errors
------

[](#errors)

All exceptions extend `Mesh0\Exception\Mesh0Exception`. The most common subclasses are:

ExceptionStatusWhen`AuthenticationException`401 / 403Missing, malformed, or revoked API key.`BadRequestException`4xxPayload rejected by validation.`NotFoundException`404Resource doesn't exist.`RateLimitException`429Inspect `->retryAfter`.`ServerException`5xxmesh0 internal error; `->errorId` set.`NetworkException`—Transport-level failure (DNS, TLS, …).`ConfigurationException`—Invalid `Config`.The transport retries idempotent failures (`5xx`, `429`, transport errors) up to `Config::maxRetries` with exponential backoff and jitter; the `Retry-After` header is honored when present.

---

Development
-----------

[](#development)

```
composer install
composer test     # PHPUnit
composer stan     # PHPStan level 9
composer cs       # PHP-CS-Fixer (PSR-12)
composer ci       # All of the above
```

---

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance97

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~4 days

Total

4

Last Release

16d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/4c0b69eb775b4823d5f2d6596189218f62ce57217753d62cfee7e8be236fa9a2?d=identicon)[jalbrecht](/maintainers/jalbrecht)

---

Top Contributors

[![xanojustin](https://avatars.githubusercontent.com/u/132400351?v=4)](https://github.com/xanojustin "xanojustin (4 commits)")

---

Tags

psr-3loggingaitracingotlploggertelemetryobservabilityllmmesh0

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/mesh0-sdk/health.svg)

```
[![Health](https://phpackages.com/badges/mesh0-sdk/health.svg)](https://phpackages.com/packages/mesh0-sdk)
```

###  Alternatives

[tempest/framework

The PHP framework that gets out of your way.

2.2k31.1k11](/packages/tempest-framework)[telnyx/telnyx-php

Official Telnyx PHP SDK — APIs for Voice, SMS, MMS, WhatsApp, Fax, SIP Trunking, Wireless IoT, Call Control, and more. Build global communications on Telnyx's private carrier-grade network.

35729.6k2](/packages/telnyx-telnyx-php)[cakephp/cakephp

The CakePHP framework

8.8k19.1M1.7k](/packages/cakephp-cakephp)[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

84735.1k](/packages/flow-php-flow)[theodo-group/llphant

LLPhant is a library to help you build Generative AI applications.

1.7k371.6k5](/packages/theodo-group-llphant)[open-telemetry/sdk

SDK for OpenTelemetry PHP.

2326.5M315](/packages/open-telemetry-sdk)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
