Skip to content
Merged
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
26 changes: 26 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
test-cli:
if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'no_run')
Expand Down Expand Up @@ -46,6 +49,29 @@ jobs:
- name: Run Integration tests
run: php tests/test_integration.php

test-d1-adapter:
if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'no_run')
name: D1 Adapter Tests (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: ['18', '20', '22']

steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Run D1 adapter tests
working-directory: d1-adapter
run: npm test
Comment thread
39ff marked this conversation as resolved.

build-extension:
if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'no_run')
name: Build Extension (PHP ${{ matrix.php-version }})
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Works with any database access method that uses mysqlnd, including PDO, mysqli,
| `cli/` | CLI profiler management tool (PHP) |
| `demo/` | Docker-based web demo (Laravel + WebSocket) |
| `jetbrains-plugin/` | JetBrains IDE plugin (Kotlin) |
| `d1-adapter/` | Cloudflare D1 (local) adapter for `wrangler dev` (JS) |

## Features

Expand All @@ -28,6 +29,7 @@ Works with any database access method that uses mysqlnd, including PDO, mysqli,
- **Prepared statement support** — Logs bound parameters (PHP 7.0+)
- **SQL analysis** — Automatic extraction of table and column names
- **Job management** — Concurrent profiling sessions with parent-child relationships
- **Cloudflare D1 (local)** — Traces D1 queries during `wrangler dev` into the same job logs (see [`d1-adapter/`](d1-adapter/))
- **Cross-platform** — Linux / macOS / Windows

## Requirements
Expand Down Expand Up @@ -123,6 +125,34 @@ $tag = mariadb_profiler_get_tag(); // 'checkout_flow'
mariadb_profiler_untag();
```

### Tracing Cloudflare D1 (local)

D1 queries executed during `wrangler dev` can be traced into the same job
logs via the [`d1-adapter/`](d1-adapter/):

```bash
# Terminal 1: start the local collector
node d1-adapter/collector.js

# Terminal 2: start a job, then exercise your Worker
php cli/mariadb_profiler.php job start d1job
```

```ts
// In your Worker (local dev only)
import { traceD1 } from './mariadb-profiler-d1';

export default {
async fetch(request, env, ctx) {
const db = traceD1(env.DB, { ctx, enabled: env.D1_TRACE === '1' });
const user = await db.prepare('SELECT * FROM users WHERE id = ?').bind(42).first();
return Response.json(user);
},
};
```

See [`d1-adapter/README.md`](d1-adapter/README.md) for details.

### Demo

```bash
Expand Down
5 changes: 5 additions & 0 deletions cli/mariadb_profiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ function cmdJobShow(JobManager $manager, $key, $tagFilter = null)
$output['trace'] = $entry['trace'];
}

// Include source database marker if present (e.g. "d1")
if (isset($entry['db']) && $entry['db'] !== '') {
$output['db'] = $entry['db'];
}

fwrite(STDOUT, json_encode($output, JSON_UNESCAPED_UNICODE) . "\n");
}
}
Expand Down
138 changes: 138 additions & 0 deletions d1-adapter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Cloudflare D1 (local) Adapter

Trace Cloudflare D1 queries during local development (`wrangler dev`) with the
same job logs, CLI tool, and IDE plugins as the PHP extension.

D1 runs inside workerd, so the PHP extension cannot hook it directly. Instead
this adapter has two small parts:

```text
┌─────────────────────────────┐ ┌─────────────────────────┐
│ wrangler dev (workerd) │ │ collector.js (Node) │
│ │ HTTP │ │
│ traceD1(env.DB) ──────────────────▶ writes jobs logs: │
│ intercepts prepare/bind/ │ POST │ {key}.jsonl │
│ run/all/first/raw/batch/ │ │ {key}.raw.log │
│ exec/withSession │ │ (same format as the │
└─────────────────────────────┘ │ PHP extension) │
└─────────────────────────┘
```

Queries are recorded only while a profiling job is active (started via the
CLI tool or the VSCode / JetBrains plugins), exactly like the extension.
When no collector is running or no job is active, queries are dropped
silently with no impact on your Worker.

## Setup

### 1. Start the collector

```bash
node d1-adapter/collector.js
# [mariadb-profiler-d1] collector listening on http://127.0.0.1:8786/
# [mariadb-profiler-d1] log dir: /tmp/mariadb_profiler
```

Options:

| Option | Default | Description |
|---|---|---|
| `--port <n>` | `8786` | Listen port (also `MARIADB_PROFILER_COLLECTOR_PORT`) |
| `--host <h>` | `127.0.0.1` | Listen host |
| `--log-dir <dir>` | `/tmp/mariadb_profiler` | Profiler log directory (also `MARIADB_PROFILER_LOG_DIR`); must match the CLI / IDE plugin setting |
| `--no-raw` | raw enabled | Disable `.raw.log` output (like `mariadb_profiler.raw_log=0`) |
| `--job-check-interval <ms>` | `1000` | How often `jobs.json` is re-read |

### 2. Wrap your D1 binding

Copy `index.js` into your Worker project (or vendor this directory and import
it). Then wrap the binding in your fetch handler:

```ts
import { traceD1 } from './mariadb-profiler-d1';

export default {
async fetch(request, env, ctx) {
// Only trace in local dev: gate with a wrangler dev var.
const db = traceD1(env.DB, { ctx, enabled: env.D1_TRACE === '1' });

db.profilerTag('user_lookup');
const user = await db.prepare('SELECT * FROM users WHERE id = ?')
.bind(42)
.first();
db.profilerUntag();

return Response.json(user);
},
};
```

`wrangler.toml`:

```toml
[vars]
D1_TRACE = "1" # set to "0" / omit in production environments
```

When `enabled` is `false`, `traceD1` returns the original binding untouched,
so the call is safe to keep in production code paths. Passing `ctx` is
strongly recommended — log delivery is registered with `ctx.waitUntil()` so
it is not cancelled when the response returns.

### 3. Start a job and run queries

```bash
php cli/mariadb_profiler.php job start d1job
# ... exercise your Worker with wrangler dev ...
php cli/mariadb_profiler.php job end d1job
php cli/mariadb_profiler.php job show d1job
```

Or start/stop jobs from the VSCode / JetBrains plugin — D1 queries appear in
the same query list and live tail as mysqlnd queries.

D1 entries carry a `"db":"d1"` marker in the `.jsonl` log (entries written by
the PHP extension have no `db` field and are treated as MariaDB). The VSCode
extension shows the source (`D1` / `MariaDB`) next to each query and as a
`Source:` row in the expanded details; the JetBrains plugin shows it in the
`DB` column of the query table and in the detail panel. `php cli/... job show`
passes the `db` field through as well.

## API

| Function | Description |
|---|---|
| `traceD1(db, options?)` | Wrap a `D1Database` (or `D1DatabaseSession`); intercepts `prepare/bind/first/run/all/raw/batch/exec/withSession` |
| `db.profilerTag(tag)` | Push a tag onto the wrapper's request-scoped stack (recommended) |
| `db.profilerUntag(tag?)` | Pop a tag from the wrapper's stack; returns the popped tag or `null` |
| `db.profilerGetTag()` | Current tag (wrapper stack first, then the shared stack), or `null` |
| `d1ProfilerTag(tag)` | Push a tag onto the **shared** stack (isolate-wide) |
| `d1ProfilerUntag(tag?)` | Pop a tag from the shared stack; returns the popped tag or `null` |
| `d1ProfilerGetTag()` | Get the current shared tag, or `null` |

Create the wrapper inside your fetch handler and use the `db.profilerTag()`
methods: each wrapper has its own tag stack, so tags never interleave across
concurrent requests. The module-level `d1ProfilerTag()` functions operate on
a single stack shared by the whole isolate — fine for simple sequential local
testing, but concurrent requests will see each other's tags.

### `traceD1` options

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | `false` returns the original binding untouched |
| `ctx` | — | Request `ExecutionContext`; used for `waitUntil()` (recommended) |
| `collectorUrl` | `http://127.0.0.1:8786/` | Collector endpoint |
| `traceDepth` | `0` | JS backtrace frames to record per query (like `mariadb_profiler.trace_depth`; `0` = disabled) |
| `fetch` | global `fetch` | Custom fetch implementation (for tests) |

Failed queries are logged with status `err` (the error is re-thrown), bound
parameters are recorded like prepared statements in the extension, and
`batch()` logs each statement individually.

## Tests

```bash
cd d1-adapter
npm test
```
Loading
Loading