Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/good-eels-open.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"jazz-tools": patch
"cojson": patch
---

Added OpenTelemetry observability support for monitoring subscription performance

- Instrumented `SubscriptionScope` with metrics (`jazz.subscription.active`, `jazz.subscription.first_load`) and tracing spans
- Added performance tracking for subscription lifecycle events including storage loading, peer fetching, and transaction parsing
- Added new "Perf" tab in Jazz Inspector to visualize subscription load times and metrics
- Added documentation for integrating Jazz with OpenTelemetry-compatible observability backends

7 changes: 2 additions & 5 deletions examples/music-player/src/2_main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ function Main() {
return (
<SidebarProvider>
<AppContent mediaPlayer={mediaPlayer} />
<JazzInspector />
</SidebarProvider>
);
}
Expand All @@ -107,10 +106,8 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
authSecretStorageKey="examples/music-player"
onAnonymousAccountDiscarded={onAnonymousAccountDiscarded}
>
<SidebarProvider>
<Main />
<JazzInspector />
</SidebarProvider>
<Main />
<JazzInspector />
</JazzReactProvider>
</React.StrictMode>,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// #region WithOpenTelemetry
import React from "react";
import { jazzMetricReader, JazzInspector } from "jazz-tools/inspector";
import { JazzReactProvider } from "jazz-tools/react";
import {
MeterProvider,
PeriodicExportingMetricReader,
ConsoleMetricExporter,
} from "@opentelemetry/sdk-metrics";
import { metrics } from "@opentelemetry/api";

// Include jazzMetricReader alongside your other readers
const meterProvider = new MeterProvider({
readers: [
// Your existing metric reader
new PeriodicExportingMetricReader({
exporter: new ConsoleMetricExporter(),
exportIntervalMillis: 10000,
}),
// Add this to enable the Inspector's Performance tab
jazzMetricReader,
],
});

metrics.setGlobalMeterProvider(meterProvider);

function App() {
return (
// @ts-expect-error No sync prop
<JazzReactProvider>
{/* Your app components */}
<JazzInspector />
</JazzReactProvider>
);
}
// #endregion

export {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// #region Metrics
import { unstable_setOpenTelemetryInstrumentationEnabled } from "jazz-tools";
import {
MeterProvider,
PeriodicExportingMetricReader,
ConsoleMetricExporter,
} from "@opentelemetry/sdk-metrics";
import { metrics } from "@opentelemetry/api";

// Enable instrumentation (required for metrics and tracing)
unstable_setOpenTelemetryInstrumentationEnabled(true);

// Create a console exporter for development
const metricExporter = new ConsoleMetricExporter();

// Set up the meter provider with periodic export
const meterProvider = new MeterProvider({
readers: [
new PeriodicExportingMetricReader({
exporter: metricExporter,
exportIntervalMillis: 10000, // Export every 10 seconds
}),
],
});

// Register the provider globally
metrics.setGlobalMeterProvider(meterProvider);
// #endregion

// #region Tracing
import { unstable_setOpenTelemetryInstrumentationEnabled } from "jazz-tools";
import {
BasicTracerProvider,
SimpleSpanProcessor,
ConsoleSpanExporter,
} from "@opentelemetry/sdk-trace-base";
import { trace } from "@opentelemetry/api";

// Enable instrumentation (required for metrics and tracing)
unstable_setOpenTelemetryInstrumentationEnabled(true);

// Create a console exporter for development
const spanExporter = new ConsoleSpanExporter();

// Set up the tracer provider
const tracerProvider = new BasicTracerProvider({
spanProcessors: [new SimpleSpanProcessor(spanExporter)],
});

// Register the provider globally
trace.setGlobalTracerProvider(tracerProvider);
// #endregion

// #region Production
import { unstable_setOpenTelemetryInstrumentationEnabled } from "jazz-tools";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import {
MeterProvider as MeterProviderProd,
PeriodicExportingMetricReader as PeriodicExportingMetricReaderProd,
} from "@opentelemetry/sdk-metrics";
import {
BasicTracerProvider as BasicTracerProviderProd,
BatchSpanProcessor,
} from "@opentelemetry/sdk-trace-base";
import { metrics as metricsProd, trace as traceProd } from "@opentelemetry/api";

// Enable instrumentation (required for metrics and tracing)
unstable_setOpenTelemetryInstrumentationEnabled(true);

// Configure OTLP exporters pointing to your collector
const otlpMetricExporter = new OTLPMetricExporter({
url: "https://your-collector.example.com/v1/metrics",
});

const otlpTraceExporter = new OTLPTraceExporter({
url: "https://your-collector.example.com/v1/traces",
});

// Set up providers with production exporters
const prodMeterProvider = new MeterProviderProd({
readers: [
new PeriodicExportingMetricReaderProd({
exporter: otlpMetricExporter,
exportIntervalMillis: 60000, // Export every minute
}),
],
});

const prodTracerProvider = new BasicTracerProviderProd({
spanProcessors: [new BatchSpanProcessor(otlpTraceExporter)],
});

metricsProd.setGlobalMeterProvider(prodMeterProvider);
traceProd.setGlobalTracerProvider(prodTracerProvider);
// #endregion
5 changes: 5 additions & 0 deletions homepage/homepage/content/docs/docNavigationItems.js
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,11 @@ export const docNavigationItems = [
href: "/docs/tooling-and-resources/inspector",
done: 100,
},
{
name: "Observability",
href: "/docs/tooling-and-resources/observability",
done: 100,
},
{
name: "AI tools (llms.txt)",
href: "/docs/tooling-and-resources/ai-tools",
Expand Down
29 changes: 29 additions & 0 deletions homepage/homepage/content/docs/tooling-and-resources/inspector.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,32 @@ Check out the [music player app](https://github.com/garden-co/jazz/blob/main/exa
<ContentByFramework framework="svelte">
Check out the [file share app](https://github.com/garden-co/jazz/blob/main/examples/file-share-svelte/src/routes/%2Blayout.svelte) for a full example.
</ContentByFramework>

## Performance Tab

The Inspector includes a Performance tab that helps you analyze the runtime behavior of your Jazz app. The Performance tab uses OpenTelemetry under the hood to collect metrics.

Metrics instrumentation is registered automatically if no OpenTelemetry configuration is found.

### Using with your existing OpenTelemetry setup

If you already have OpenTelemetry configured in your frontend, you should add `jazzMetricReader` to your MeterProvider's readers array to enable the Performance tab:

<CodeGroup>
```tsx index.tsx#WithOpenTelemetry
```
</CodeGroup>

This allows the Inspector's Performance tab to access the metrics while your existing observability setup continues to work normally.

### What gets tracked

The Performance tab displays several metrics:

- **Active subscriptions** — The number of CoValue subscriptions currently active in your app
- **First load time** — How long it takes for a subscription to receive its first data
- **Load breakdown** — Detailed timing for storage reads, peer fetches, and transaction parsing

These metrics help you identify slow-loading CoValues, understand where time is spent during data loading, and optimize your app's performance.

The Performance tab also integrates with Chrome DevTools' Performance panel, allowing you to see Jazz-specific timing marks alongside your other performance data.
154 changes: 154 additions & 0 deletions homepage/homepage/content/docs/tooling-and-resources/observability.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { CodeGroup } from '@/components/forMdx'

export const metadata = {
description: "Unstable API: Learn how to integrate Jazz with OpenTelemetry for metrics and tracing."
};

# Observability

<div className="not-prose rounded-lg border border-amber-500/50 bg-amber-500/10 px-4 py-3 text-sm text-amber-900 dark:text-amber-100">
⚠️ **Unstable API**: The observability API is experimental and subject to breaking changes in future releases. Use with caution in production environments.
</div>

Jazz exposes metrics and spans using the [OpenTelemetry](https://opentelemetry.io/) protocol. This allows you to integrate Jazz's internal performance data with your existing observability stack, whether that's a console logger for development or a full observability platform in production.

## Enabling instrumentation

OpenTelemetry instrumentation is **opt-in** and disabled by default for performance reasons. To enable it, call `unstable_setOpenTelemetryInstrumentationEnabled` before setting up your OpenTelemetry providers:

```ts
import { unstable_setOpenTelemetryInstrumentationEnabled } from "jazz-tools";

// Enable instrumentation - do this before setting up your OpenTelemetry providers
unstable_setOpenTelemetryInstrumentationEnabled(true);
```

<div className="not-prose rounded-lg border border-blue-500/50 bg-blue-500/10 px-4 py-3 text-sm text-blue-900 dark:text-blue-100">
💡 **Note**: You only need to enable instrumentation when you want to collect metrics or traces. If you're not using OpenTelemetry, leave it disabled to avoid any performance overhead.
</div>

## Available metrics

Jazz tracks the following metrics:

<table>
<thead>
<tr>
<th>Metric</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>jazz.subscription.active</code></td>
<td>UpDownCounter</td>
<td>Number of active CoValue subscriptions</td>
</tr>
<tr>
<td><code>jazz.subscription.first_load</code></td>
<td>Histogram</td>
<td>Time for a subscription to receive its first data (ms)</td>
</tr>
<tr>
<td><code>jazz.peers</code></td>
<td>UpDownCounter</td>
<td>Number of connected peers</td>
</tr>
<tr>
<td><code>jazz.transactions.size</code></td>
<td>Histogram</td>
<td>Size of transactions (bytes)</td>
</tr>
<tr>
<td><code>jazz.usage.ingress</code></td>
<td>Counter</td>
<td>Total bytes received from peers</td>
</tr>
<tr>
<td><code>jazz.usage.egress</code></td>
<td>Counter</td>
<td>Total bytes sent to peers</td>
</tr>
</tbody>
</table>

## Available spans

Jazz creates spans to track the subscription lifecycle:

<table>
<thead>
<tr>
<th>Span</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>jazz.subscription</code></td>
<td>Parent span for the entire subscription lifecycle</td>
</tr>
<tr>
<td><code>jazz.subscription.first_load</code></td>
<td>Span for the initial data load</td>
</tr>
<tr>
<td><code>jazz.subscription.first_load.from_storage</code></td>
<td>Time spent loading from local storage</td>
</tr>
<tr>
<td><code>jazz.subscription.first_load.load_from_peer</code></td>
<td>Time spent fetching from a peer</td>
</tr>
<tr>
<td><code>jazz.subscription.first_load.transaction_parsing</code></td>
<td>Time spent parsing transactions</td>
</tr>
</tbody>
</table>

## Setting up metrics

To collect metrics, you need to:
1. Enable instrumentation using `unstable_setOpenTelemetryInstrumentationEnabled(true)`
2. Configure an OpenTelemetry `MeterProvider` with a reader that exports the data

Here's a simple example that logs metrics to the console:

<CodeGroup>
```ts tooling-and-resources/observability/observability.ts#Metrics
```
</CodeGroup>

**Tip:** If you're using the [Jazz Inspector](/docs/tooling-and-resources/inspector#performance-tab) in your app, add `jazzMetricReader` from `jazz-tools/inspector` to your MeterProvider's readers array to enable the Performance tab.

## Setting up tracing

To collect spans, you need to:
1. Enable instrumentation using `unstable_setOpenTelemetryInstrumentationEnabled(true)`
2. Configure a `TracerProvider` with a span processor

Here's an example that logs spans to the console:

<CodeGroup>
```ts tooling-and-resources/observability/observability.ts#Tracing
```
</CodeGroup>

Check the [OpenTelemetry JS documentation](https://opentelemetry.io/docs/languages/js/) for more details on available exporters and configuration options.

## Production setup

In production, you'll typically want to use OTLP exporters to send data to your observability platform (like Jaeger, Prometheus, or a managed service). Here's a complete example:

<CodeGroup>
```ts tooling-and-resources/observability/observability.ts#Production
```
</CodeGroup>

This example uses HTTP-based OTLP exporters, but you can also use gRPC-based exporters depending on your infrastructure. The key differences from development setup are:

- Using `BatchSpanProcessor` for more efficient span batching in production
- Longer export intervals (60 seconds vs 10 seconds) to reduce network overhead
- OTLP exporters that send data to your collector or observability platform
6 changes: 5 additions & 1 deletion homepage/homepage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@
"@biomejs/biome": "catalog:default",
"@evilmartians/harmony": "^1.4.0",
"@next/mdx": "^15.3.3",
"@opentelemetry/exporter-metrics-otlp-http": "^0.208.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
"@opentelemetry/sdk-metrics": "^2.2.0",
"@opentelemetry/sdk-trace-base": "^2.2.0",
"@playwright/test": "^1.52.0",
"@tailwindcss/postcss": "^4.1.16",
"@tailwindcss/typography": "^0.5.16",
Expand All @@ -83,8 +87,8 @@
"postcss": "^8",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.7.1",
"tailwindcss": "^4.1.16",
"svelte-check": "^4.3.3",
"tailwindcss": "^4.1.16",
"turbo": "^2.3.1",
"typescript": "catalog:default",
"unified": "^11.0.5",
Expand Down
Loading
Loading