Skip to content

Commit bf91f8e

Browse files
committed
feat: allow to publish logs
1 parent 35b360d commit bf91f8e

30 files changed

Lines changed: 1081 additions & 80 deletions

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ PHP-FPM / CLI Go Forwarder Collector
3636
└─────────┘
3737
```
3838

39-
Spans are serialized as compact msgpack and sent via UDP to a local Go forwarder, which batches and forwards them to your OTLP collector.
39+
Spans are serialized as compact msgpack and sent via UDP to a local Go forwarder, which batches and forwards them to your OTLP collector. Log records emitted with `Akari\log()` travel the same UDP path and are forwarded to the collector's `/v1/logs` endpoint (spans go to `/v1/traces`).
4040

4141
---
4242

@@ -204,9 +204,9 @@ use function Akari\{
204204
enable, disable, createSpan,
205205
setTransactionName, getTransactionName, setServiceName,
206206
addTag, getTags, removeTag, setCustomVariable,
207-
logException, generateDistributedTracingHeaders,
207+
logException, log, generateDistributedTracingHeaders,
208208
markAsWebTransaction, markAsCliTransaction,
209-
isProfiling, getSpanCount, getSpansJson
209+
isProfiling, getSpanCount, getSpansJson, getLogsJson
210210
};
211211

212212
// Manual control
@@ -216,6 +216,10 @@ setTransactionName('POST /checkout');
216216
addTag('customer_id', '42');
217217
logException($e);
218218

219+
// OTLP logs — PSR-3 style. Each record carries the active trace_id and the
220+
// current (or root) span_id for correlation, and is forwarded to /v1/logs.
221+
log('warning', 'payment retry', ['attempt' => 2, 'gateway' => 'stripe']);
222+
219223
// W3C traceparent header for manual propagation
220224
$headers = generateDistributedTracingHeaders();
221225
// → ['traceparent' => '00-abc123...-def456...-01']
@@ -224,7 +228,8 @@ disable();
224228

225229
// Introspection
226230
echo getSpanCount(); // number of spans this request
227-
echo getSpansJson(); // OTLP JSON for debugging
231+
echo getSpansJson(); // OTLP traces JSON for debugging
232+
echo getLogsJson(); // OTLP logs JSON for debugging
228233
```
229234

230235
---

compose.yml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ services:
2121
build:
2222
context: forwarder
2323
environment:
24-
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318
24+
- OTEL_EXPORTER_OTLP_ENDPOINT=http://lgtm:4318
2525
- OTEL_FORWARDER_LISTEN=0.0.0.0:4319
26+
depends_on:
27+
- lgtm
2628

27-
jaeger:
28-
image: jaegertracing/all-in-one:latest
29+
# Grafana LGTM: Loki (logs) + Grafana + Tempo (traces) + Prometheus, with an
30+
# OTLP receiver on 4317/4318. Traces land in Tempo, logs in Loki, both
31+
# viewable (and cross-linked) in Grafana.
32+
lgtm:
33+
image: grafana/otel-lgtm:latest
2934
ports:
30-
- "16686:16686"
31-
- "4318:4318"
32-
environment:
33-
- COLLECTOR_OTLP_ENABLED=true
35+
- "3000:3000" # Grafana UI

demo/README.md

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,47 @@
11
# akari demo
22

3-
See traces from PHP applications in Jaeger in 30 seconds.
3+
See traces and logs from PHP applications in Grafana in 30 seconds.
44

55
## Quick start
66

77
```bash
8-
cd demo
98
docker compose up --build
109
```
1110

1211
Then open:
13-
- **Simple demo app**: http://localhost:8081
12+
- **Simple demo app**: http://localhost:8083
1413
- **Symfony Demo**: http://localhost:8082
15-
- **Jaeger UI**: http://localhost:16686
14+
- **Grafana UI**: http://localhost:3000
1615

1716
## Services
1817

1918
| Service | Port | Description |
2019
|---------|------|-------------|
21-
| Simple PHP demo | [localhost:8081](http://localhost:8081) | Basic pages showing PDO, curl, closures |
20+
| Simple PHP demo | [localhost:8083](http://localhost:8083) | Basic pages showing PDO, curl, closures, and `Akari\log()` |
2221
| Symfony Demo | [localhost:8082](http://localhost:8082) | Full Symfony app with route detection |
23-
| Jaeger UI | [localhost:16686](http://localhost:16686) | Trace viewer |
22+
| Grafana (LGTM) | [localhost:3000](http://localhost:3000) | Traces (Tempo) + logs (Loki) viewer |
23+
24+
The forwarder sends spans to the collector's `/v1/traces` and log records to
25+
`/v1/logs`; the bundled Grafana LGTM stack stores traces in Tempo and logs in
26+
Loki.
2427

2528
## Viewing traces
2629

2730
1. Browse the demo apps to generate traffic
28-
2. Open [Jaeger UI](http://localhost:16686)
29-
3. Select service **demo-php-app** or **symfony-demo**
30-
4. Click **Find Traces**
31-
5. Click a trace to see the span waterfall
31+
2. Open [Grafana](http://localhost:3000)**Explore**
32+
3. Select the **Tempo** datasource → **Search**, filter by service `demo-php-app` or `symfony-demo`
33+
4. Click a trace to see the span waterfall
34+
35+
## Viewing logs
36+
37+
The `/logging.php` page (and `/complex.php`) emit structured log records with
38+
`Akari\log()`. Each record carries the request's `trace_id` and the active
39+
`span_id`, so logs and traces are linked.
40+
41+
1. Open http://localhost:8083/logging.php to generate some logs
42+
2. In [Grafana](http://localhost:3000)**Explore**, select the **Loki** datasource
43+
3. Query `{service_name="demo-php-app"}`
44+
4. Expand a log line — `trace_id` / `span_id` are attached; click the trace id to jump to the trace in Tempo
3245

3346
### What you'll see in Symfony Demo traces
3447

@@ -39,5 +52,7 @@ Then open:
3952
## Architecture
4053

4154
```
42-
Browser → PHP/Apache → UDP/msgpack → Go forwarder → OTLP/HTTP → Jaeger
55+
Browser → PHP/Apache → UDP/msgpack → Go forwarder ──OTLP/HTTP──> Grafana LGTM
56+
├── /v1/traces → Tempo
57+
└── /v1/logs → Loki
4358
```

demo/app/complex.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
<?php
22
/**
3-
* Combined demo — shows all instrumentation features in one trace.
3+
* Combined demo — shows all instrumentation features in one trace, plus a
4+
* correlated OTLP log record via Akari\log().
45
*/
56

7+
use function Akari\log;
8+
69
class UserService {
710
private PDO $db;
811

@@ -52,6 +55,9 @@ public function handle(): string {
5255
// Fetch users from DB
5356
$users = $userService->findAll();
5457

58+
// Emit a structured log correlated to this trace.
59+
log('info', 'loaded users', ['count' => count($users)]);
60+
5561
// Enrich with avatar URLs (outgoing HTTP)
5662
$enriched = array_map(function($user) use ($apiClient) {
5763
$user['avatar'] = $apiClient->fetchUserAvatar($user['email']);
@@ -68,6 +74,7 @@ private function render(array $users): string {
6874
$html .= "<li>INTERNAL spans (PHP functions/methods)</li>";
6975
$html .= "<li>CLIENT spans (PDO queries with db.statement)</li>";
7076
$html .= "<li>CLIENT spans (curl requests with traceparent injection)</li>";
77+
$html .= "<li>An OTLP log record (Akari\\log) carrying this trace's id</li>";
7178
$html .= "</ul>";
7279
$html .= "<table border='1' cellpadding='5'>";
7380
$html .= "<tr><th>Name</th><th>Email</th><th>Avatar</th></tr>";
@@ -76,7 +83,7 @@ private function render(array $users): string {
7683
$html .= "<tr><td>{$user['name']}</td><td>{$user['email']}</td><td>{$avatar}</td></tr>";
7784
}
7885
$html .= "</table>";
79-
$html .= "<p><em>Open <a href='http://localhost:16686'>Jaeger</a>, select service 'demo-php-app', and click 'Find Traces'</em></p>";
86+
$html .= "<p><em>Open <a href='http://localhost:3000'>Grafana</a> → Explore → Tempo for the trace, and Loki ({service_name=\"demo-php-app\"}) for the log.</em></p>";
8087
return $html;
8188
}
8289
}

demo/app/database.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@
3232
echo "<tr><td>{$row['id']}</td><td>{$row['name']}</td><td>{$row['email']}</td></tr>";
3333
}
3434
echo "</table>";
35-
echo "<p><em>Check Jaeger — you'll see CLIENT spans with db.system=sqlite and db.statement</em></p>";
35+
echo "<p><em>Check <a href='http://localhost:3000'>Grafana</a> (Explore → Tempo) — you'll see CLIENT spans with db.system=sqlite and db.statement</em></p>";

demo/app/hello.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ private function format(string $message): string {
3131
echo $greeting;
3232
echo "<p>fibonacci(5) = {$fib}</p>";
3333
echo "<p>transform('abc') = {$transformed}</p>";
34-
echo "<p><em>Check <a href='http://localhost:16686'>Jaeger</a> for traces — look for service 'demo-php-app'</em></p>";
34+
echo "<p><em>Check <a href='http://localhost:3000'>Grafana</a> (Explore → Tempo) for traces — look for service 'demo-php-app'</em></p>";

demo/app/http-client.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ function fetchUrl(string $url): array {
1313
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
1414
curl_setopt($ch, CURLOPT_HTTPHEADER, [
1515
'Accept: application/json',
16-
'X-Demo: otel_tracer',
16+
'X-Demo: akari',
1717
]);
1818
$body = curl_exec($ch);
1919
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
@@ -42,4 +42,4 @@ function fetchUrl(string $url): array {
4242
echo "<h3>Injected traceparent header</h3>";
4343
echo "<pre>{$traceparent}</pre>";
4444
echo "<p>Format: <code>00-{trace_id}-{span_id}-01</code></p>";
45-
echo "<p><em>Check Jaeger — you'll see CLIENT spans with url.full, http.request.method, and the traceparent links requests to this trace</em></p>";
45+
echo "<p><em>Check <a href='http://localhost:3000'>Grafana</a> (Explore → Tempo) — you'll see CLIENT spans with url.full, http.request.method, and the traceparent links requests to this trace</em></p>";

demo/app/index.php

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,31 @@
11
<?php
22
/**
3-
* otel_tracer demo application
3+
* akari demo application
44
*
5-
* Hit these endpoints to generate traces visible in Jaeger UI:
6-
* http://localhost:8081/ — this page (links)
7-
* http://localhost:8081/hello.php — simple function tracing
8-
* http://localhost:8081/database.php — PDO/SQLite queries
9-
* http://localhost:8081/http-client.php — outgoing curl with traceparent
10-
* http://localhost:8081/complex.php — all features combined
5+
* Hit these endpoints to generate traces (and logs) visible in Grafana:
6+
* http://localhost:8083/ — this page (links)
7+
* http://localhost:8083/hello.php — simple function tracing
8+
* http://localhost:8083/database.php — PDO/SQLite queries
9+
* http://localhost:8083/http-client.php — outgoing curl with traceparent
10+
* http://localhost:8083/logging.php — OTLP logs via Akari\log()
11+
* http://localhost:8083/complex.php — all features combined
1112
*
12-
* Jaeger UI: http://localhost:16686
13+
* Grafana UI: http://localhost:3000 (Explore → Tempo for traces, Loki for logs)
1314
*/
1415
?>
1516
<!DOCTYPE html>
1617
<html>
17-
<head><title>otel_tracer demo</title></head>
18+
<head><title>akari demo</title></head>
1819
<body>
19-
<h1>otel_tracer demo</h1>
20-
<p>Extension loaded: <strong><?= extension_loaded('otel_tracer') ? 'yes' : 'no' ?></strong></p>
20+
<h1>akari demo</h1>
21+
<p>Extension loaded: <strong><?= extension_loaded('akari') ? 'yes' : 'no' ?></strong></p>
2122
<ul>
2223
<li><a href="/hello.php">Simple function tracing</a></li>
2324
<li><a href="/database.php">Database queries (PDO/SQLite)</a></li>
2425
<li><a href="/http-client.php">Outgoing HTTP (curl + traceparent)</a></li>
26+
<li><a href="/logging.php">OTLP logs (Akari\log)</a></li>
2527
<li><a href="/complex.php">All features combined</a></li>
2628
</ul>
27-
<p>Open <a href="http://localhost:16686" target="_blank">Jaeger UI</a> to see traces.</p>
29+
<p>Open <a href="http://localhost:3000" target="_blank">Grafana</a> to see traces (Tempo) and logs (Loki).</p>
2830
</body>
2931
</html>

demo/app/logging.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
/**
3+
* OTLP logs demo with Akari\log().
4+
*
5+
* Shows: structured log records emitted from PHP and correlated to the current
6+
* trace. Each Akari\log() call attaches the request's trace_id and the active
7+
* span_id, so in Grafana you can jump from a log line straight to its trace
8+
* (and back). In this web request the active span is the request's root span,
9+
* so every log below correlates to that trace.
10+
*
11+
* Severity is a PSR-3 level string; the context array becomes log attributes.
12+
*/
13+
14+
use function Akari\log;
15+
16+
class Checkout
17+
{
18+
public function process(string $orderId, int $amountCents): bool
19+
{
20+
log('info', 'checkout started', [
21+
'order_id' => $orderId,
22+
'amount_cents' => $amountCents,
23+
'currency' => 'EUR',
24+
]);
25+
26+
$attempt = 0;
27+
$captured = false;
28+
while ($attempt < 3 && !$captured) {
29+
$attempt++;
30+
// Pretend the first attempt is declined, then it succeeds.
31+
$captured = $attempt >= 2;
32+
33+
if (!$captured) {
34+
log('warning', 'payment declined, retrying', [
35+
'order_id' => $orderId,
36+
'attempt' => $attempt,
37+
'gateway' => 'stripe',
38+
]);
39+
}
40+
}
41+
42+
if ($captured) {
43+
log('info', 'payment captured', [
44+
'order_id' => $orderId,
45+
'attempts' => $attempt,
46+
]);
47+
} else {
48+
log('error', 'payment failed after retries', [
49+
'order_id' => $orderId,
50+
'attempts' => $attempt,
51+
]);
52+
}
53+
54+
return $captured;
55+
}
56+
}
57+
58+
$checkout = new Checkout();
59+
$ok = $checkout->process('ORD-' . random_int(1000, 9999), 4999);
60+
61+
// A final request-scope log line.
62+
log($ok ? 'notice' : 'error', 'checkout finished', ['success' => $ok]);
63+
64+
echo "<h2>OTLP Logs Demo</h2>";
65+
echo "<p>This request emitted structured log records via <code>Akari\\log()</code>:</p>";
66+
echo "<ul>";
67+
echo "<li><code>info</code> — checkout started (order + amount attributes)</li>";
68+
echo "<li><code>warning</code> — payment declined, retrying (attempt 1)</li>";
69+
echo "<li><code>info</code> — payment captured</li>";
70+
echo "<li><code>" . ($ok ? 'notice' : 'error') . "</code> — checkout finished</li>";
71+
echo "</ul>";
72+
echo "<p>Each record carries the request's <code>trace_id</code> and the active "
73+
. "<code>span_id</code>, so logs and traces are linked.</p>";
74+
echo "<p><em>Open <a href='http://localhost:3000'>Grafana</a> → Explore → "
75+
. "<strong>Loki</strong> datasource, query <code>{service_name=\"demo-php-app\"}</code>, "
76+
. "then click a log line's trace ID to jump to the trace in Tempo.</em></p>";
77+
78+
// For quick inspection without a backend, the in-memory OTLP logs JSON:
79+
echo "<details><summary>Raw OTLP logs JSON (debug)</summary><pre>";
80+
echo htmlspecialchars(json_encode(
81+
json_decode(Akari\getLogsJson(), true),
82+
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
83+
));
84+
echo "</pre></details>";

forwarder/cmd/akari-forwarder/main.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,21 @@ func flushLoop(ctx context.Context, buf *buffer.Buffer, fwd forwarder.Forwarder,
8989

9090
func forwardBatch(ctx context.Context, batch [][]byte, fwd forwarder.Forwarder) {
9191
for _, payload := range batch {
92-
otlpData, err := transform.Transform(payload)
92+
res, err := transform.Transform(payload)
9393
if err != nil {
9494
log.Printf("transform error (%d bytes): %v", len(payload), err)
9595
continue
9696
}
9797

98-
if err := fwd.Forward(ctx, otlpData); err != nil {
99-
log.Printf("forward error: %v", err)
98+
if len(res.Traces) > 0 {
99+
if err := fwd.Forward(ctx, res.Traces, forwarder.SignalTraces); err != nil {
100+
log.Printf("forward traces error: %v", err)
101+
}
102+
}
103+
if len(res.Logs) > 0 {
104+
if err := fwd.Forward(ctx, res.Logs, forwarder.SignalLogs); err != nil {
105+
log.Printf("forward logs error: %v", err)
106+
}
100107
}
101108
}
102109
}

0 commit comments

Comments
 (0)