Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- Cache observability is now built on [Koriym.SemanticLogger](https://github.com/koriym/Koriym.SemanticLogger): an open/event/close tree whose nesting **is** the embed/dependency structure (a parent's embedded children nest under it). Typed `AbstractContext` subclasses live in `src/Log/Context/` with per-context JSON Schemas in `docs/schemas/context/`.
- `SafeSemanticLogger` (best-effort decorator) guarantees logging never breaks cache reads/writes; `NullSemanticLogger` is the zero-cost no-op default. Bound via `SafeSemanticLoggerProvider` in `DonutCacheModule`.
- `invalidate` context records per-target outcomes as self-describing status words: `roPool`/`etagPool` (`invalidated`|`failed`), `cdn` (`purged`|`failed`), plus `durationMs`. The CDN purge is best-effort and no longer fails local invalidation on outage.
- Logs validate against their schemas in tests via `SemanticLogValidator` (`SemanticLogTreeTrait`), and `vendor/bin/stree` renders the cache log as a readable tree (`demo/run-dependency.php`, `demo/run-donut.php`).
- Direct (non-AOP) top-level `put()` and `invalidateTags()` calls are rooted in `manual_store` / `manual_invalidate` scopes so their save/invalidate events stay visible; an event with no enclosing scope would otherwise be dropped at flush.

### Deprecated
- `RepositoryLogger`, `RepositoryLoggerInterface`, `StructuredRepositoryLoggerInterface`, `NullRepositoryLogger` and `docs/schemas/repository-log.json`. Internal cache code now logs through `Koriym\SemanticLogger\SemanticLoggerInterface`; the legacy flat interface remains bound for BC but receives no internal events.

### Changed
- Cache logging call sites (`QueryRepository`, `ResourceStorage`, `DonutRepository`, `CacheInterceptor`, `AbstractDonutCacheInterceptor`, `CommandInterceptor`, `RefreshInterceptor`) now emit typed contexts through `SemanticLoggerInterface` instead of `RepositoryLoggerInterface::log()`.
- Added runtime dependency `koriym/semantic-logger`.

## [1.16.1] - 2026-06-01

### Fixed
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"php": "^8.2",
"bear/resource": "^1.16.1",
"bear/sunday": "^1.5",
"koriym/semantic-logger": "^0.8.0",
"psr/cache": "^1.0 || ^2.0 || ^3.0",
"ray/aop": "^2.19.1",
"ray/di": "^2.20",
Expand Down
60 changes: 26 additions & 34 deletions demo/run-dependency.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
use BEAR\QueryRepository\FakeEtagPoolModule;
use BEAR\QueryRepository\ModuleFactory;
use BEAR\QueryRepository\QueryRepositoryInterface;
use BEAR\QueryRepository\RepositoryLoggerInterface;
use BEAR\Resource\ResourceInterface;
use BEAR\Resource\Uri;
use Koriym\SemanticLogger\SemanticLoggerInterface;
use Koriym\SemanticLogger\Stree\RenderConfig;
use Koriym\SemanticLogger\Stree\TreeRenderer;
use Ray\Di\Injector;

require dirname(__DIR__) . '/vendor/autoload.php';
Expand Down Expand Up @@ -65,36 +67,26 @@

$resource = $injector->getInstance(ResourceInterface::class);
$repository = $injector->getInstance(QueryRepositoryInterface::class);
$logger = $injector->getInstance(RepositoryLoggerInterface::class);

// Execute scenarios silently
$logger->log('request-start', ['uri' => 'page://self/dep/level-one']);
$resource->get('page://self/dep/level-one'); // 1. Initial access

$logger->log('request-start', ['uri' => 'page://self/dep/level-one']);
$resource->get('page://self/dep/level-one'); // 2. Re-access (cache-hit)

$logger->log('request-start', ['uri' => 'page://self/dep/level-three', 'method' => 'purge']);
$repository->purge(new Uri('page://self/dep/level-three')); // 3. Purge grandchild

$logger->log('request-start', ['uri' => 'page://self/dep/level-one']);
$resource->get('page://self/dep/level-one'); // 4. Re-access after purge

$logger->log('request-start', ['uri' => 'page://self/dep/parent-a']);
$resource->get('page://self/dep/parent-a'); // 5a. Access ParentA

$logger->log('request-start', ['uri' => 'page://self/dep/parent-b']);
$resource->get('page://self/dep/parent-b'); // 5b. Access ParentB

$logger->log('request-start', ['uri' => 'page://self/dep/child-c', 'method' => 'purge']);
$repository->purge(new Uri('page://self/dep/child-c')); // 6. Purge shared child

$logger->log('request-start', ['uri' => 'page://self/dep/parent-a']);
$resource->get('page://self/dep/parent-a'); // 7a. Re-access ParentA

$logger->log('request-start', ['uri' => 'page://self/dep/parent-b']);
$resource->get('page://self/dep/parent-b'); // 7b. Re-access ParentB

// Output logs only
echo "=== Cache Log ===" . PHP_EOL;
echo $logger . PHP_EOL;
$logger = $injector->getInstance(SemanticLoggerInterface::class);

// Execute scenarios. Embedded child GETs nest under their parent GET, so the
// log's open/close tree IS the embed/dependency tree (no reconstruction).
$resource->get('page://self/dep/level-one'); // 1. Initial access (cache-miss chain)
$resource->get('page://self/dep/level-one'); // 2. Re-access (cache-hit)
$repository->purge(new Uri('page://self/dep/level-three')); // 3. Purge grandchild (cascade)
$resource->get('page://self/dep/level-one'); // 4. Re-access after purge (rebuilt)
$resource->get('page://self/dep/parent-a'); // 5a. Access ParentA
$resource->get('page://self/dep/parent-b'); // 5b. Access ParentB
$repository->purge(new Uri('page://self/dep/child-c')); // 6. Purge shared child
$resource->get('page://self/dep/parent-a'); // 7a. Re-access ParentA
$resource->get('page://self/dep/parent-b'); // 7b. Re-access ParentB

$log = $logger->flush();

// Human/AI-readable tree (open = embed scope, close = hit/miss, events = saves/invalidations)
echo "=== Cache Log Tree ===" . PHP_EOL;
echo (new TreeRenderer())->render($log->toArray(), new RenderConfig(true, 0.0, 1000, true)) . PHP_EOL;

// Machine-readable, schema-validated JSON (also: `vendor/bin/stree <file>`)
echo PHP_EOL . "=== Cache Log JSON ===" . PHP_EOL;
echo json_encode($log, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL;
29 changes: 12 additions & 17 deletions demo/run-donut.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
use BEAR\QueryRepository\FakeEtagPoolModule;
use BEAR\QueryRepository\ModuleFactory;
use BEAR\QueryRepository\QueryRepositoryInterface;
use BEAR\QueryRepository\RepositoryLoggerInterface;
use BEAR\QueryRepository\ResourceStorageInterface;
use BEAR\QueryRepository\UriTag;
use BEAR\Resource\ResourceInterface;
use BEAR\Resource\Uri;
use Koriym\SemanticLogger\SemanticLoggerInterface;
use Koriym\SemanticLogger\Stree\RenderConfig;
use Koriym\SemanticLogger\Stree\TreeRenderer;
use Madapaja\TwigModule\TwigModule;
use Ray\Di\Injector;

Expand Down Expand Up @@ -62,21 +64,14 @@
$resource = $injector->getInstance(ResourceInterface::class);
$repository = $injector->getInstance(QueryRepositoryInterface::class);
$storage = $injector->getInstance(ResourceStorageInterface::class);
$logger = $injector->getInstance(RepositoryLoggerInterface::class);
$logger = $injector->getInstance(SemanticLoggerInterface::class);

// Execute scenarios silently
$logger->log('request-start', ['uri' => 'page://self/html/blog-posting']);
$resource->get('page://self/html/blog-posting'); // 1. Initial access
// Execute scenarios. The donut GET scope wraps the embedded comment fetch.
$resource->get('page://self/html/blog-posting'); // 1. Initial access
$resource->get('page://self/html/blog-posting'); // 2. Re-access (cache-hit)
$repository->purge(new Uri('page://self/html/comment')); // 3. Manual purge of comment (top-level)
$resource->get('page://self/html/blog-posting'); // 4. Access after invalidation

$logger->log('request-start', ['uri' => 'page://self/html/blog-posting']);
$resource->get('page://self/html/blog-posting'); // 2. Re-access (cache-hit)

$logger->log('request-start', ['uri' => 'page://self/html/comment', 'method' => 'invalidate']);
$storage->invalidateTags([(new UriTag())(new Uri('page://self/html/comment'))]); // 3. Invalidate comment

$logger->log('request-start', ['uri' => 'page://self/html/blog-posting']);
$resource->get('page://self/html/blog-posting'); // 4. Access after invalidation

// Output logs only
echo "=== Cache Log ===" . PHP_EOL;
echo $logger . PHP_EOL;
// Human/AI-readable tree (open = embed scope, close = hit/miss, events = saves/invalidations)
echo "=== Cache Log Tree ===" . PHP_EOL;
echo (new TreeRenderer())->render($logger->flush()->toArray(), new RenderConfig(true, 0.0, 1000, true)) . PHP_EOL;
21 changes: 21 additions & 0 deletions docs/schemas/context/cache_hit.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bearsunday.github.io/BEAR.QueryRepository/schemas/context/cache_hit.json",
"title": "cache_hit",
"description": "Close/event: a cache lookup hit at the given layer.",
"type": "object",
"required": [
"layer"
],
"properties": {
"layer": {
"type": "string",
"enum": [
"resource",
"donut",
"donut-view"
]
}
},
"additionalProperties": false
}
21 changes: 21 additions & 0 deletions docs/schemas/context/cache_miss.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bearsunday.github.io/BEAR.QueryRepository/schemas/context/cache_miss.json",
"title": "cache_miss",
"description": "Close/event: a cache lookup miss at the given layer.",
"type": "object",
"required": [
"layer"
],
"properties": {
"layer": {
"type": "string",
"enum": [
"resource",
"donut",
"donut-view"
]
}
},
"additionalProperties": false
}
36 changes: 36 additions & 0 deletions docs/schemas/context/command.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bearsunday.github.io/BEAR.QueryRepository/schemas/context/command.json",
"title": "command",
"description": "Open: a write/command scope (#[Refresh]/#[Purge]); purges nest under it.",
"type": "object",
"required": [
"method",
"annotations"
],
"properties": {
"method": {
"type": "string"
},
"annotations": {
"type": "array",
"items": {
"type": "object",
"required": [
"class",
"uri"
],
"properties": {
"class": {
"type": "string"
},
"uri": {
"type": "string"
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}
16 changes: 16 additions & 0 deletions docs/schemas/context/command_result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bearsunday.github.io/BEAR.QueryRepository/schemas/context/command_result.json",
"title": "command_result",
"description": "Close of a command scope: the resulting HTTP status code.",
"type": "object",
"required": [
"code"
],
"properties": {
"code": {
"type": "integer"
}
},
"additionalProperties": false
}
27 changes: 27 additions & 0 deletions docs/schemas/context/depends_on.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bearsunday.github.io/BEAR.QueryRepository/schemas/context/depends_on.json",
"title": "depends_on",
"description": "Event: a parent now depends on a child (dependency-graph edge).",
"type": "object",
"required": [
"parent",
"child",
"childTags"
],
"properties": {
"parent": {
"type": "string"
},
"child": {
"type": "string"
},
"childTags": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
}
16 changes: 16 additions & 0 deletions docs/schemas/context/get.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bearsunday.github.io/BEAR.QueryRepository/schemas/context/get.json",
"title": "get",
"description": "Open: a resource (or donut) GET scope; embedded child GETs nest under it.",
"type": "object",
"required": [
"uri"
],
"properties": {
"uri": {
"type": "string"
}
},
"additionalProperties": false
}
43 changes: 43 additions & 0 deletions docs/schemas/context/invalidate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bearsunday.github.io/BEAR.QueryRepository/schemas/context/invalidate.json",
"title": "invalidate",
"description": "Event: tag invalidation outcome across the local pools and the CDN purger.",
"type": "object",
"required": [
"tags",
"roPool",
"etagPool",
"cdn",
"durationMs"
],
"properties": {
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"roPool": {
"description": "Outcome of invalidating the tags in the Resource Object pool",
"type": "string",
"enum": ["invalidated", "failed"]
},
"etagPool": {
"description": "Outcome of invalidating the tags in the ETag pool",
"type": "string",
"enum": ["invalidated", "failed"]
},
"cdn": {
"description": "Outcome of the best-effort CDN surrogate-key purge",
"type": "string",
"enum": ["purged", "failed"]
},
"durationMs": {
"description": "Wall-clock duration of the invalidation in milliseconds",
"type": "number",
"minimum": 0
}
},
"additionalProperties": false
}
15 changes: 15 additions & 0 deletions docs/schemas/context/manual_invalidate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bearsunday.github.io/BEAR.QueryRepository/schemas/context/manual_invalidate.json",
"title": "manual_invalidate",
"description": "Open: an application-initiated (manual) tag invalidation.",
"type": "object",
"required": ["tags"],
"properties": {
"tags": {
"type": "array",
"items": { "type": "string" }
}
},
"additionalProperties": false
}
12 changes: 12 additions & 0 deletions docs/schemas/context/manual_purge.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bearsunday.github.io/BEAR.QueryRepository/schemas/context/manual_purge.json",
"title": "manual_purge",
"description": "Open: an application-initiated (manual) purge of a URI.",
"type": "object",
"required": ["uri"],
"properties": {
"uri": { "type": "string" }
},
"additionalProperties": false
}
12 changes: 12 additions & 0 deletions docs/schemas/context/manual_purge_result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bearsunday.github.io/BEAR.QueryRepository/schemas/context/manual_purge_result.json",
"title": "manual_purge_result",
"description": "Close of a manual_purge scope: outcome of the local-pool invalidation.",
"type": "object",
"required": ["result"],
"properties": {
"result": { "type": "string", "enum": ["purged", "failed"] }
},
"additionalProperties": false
}
12 changes: 12 additions & 0 deletions docs/schemas/context/manual_store.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bearsunday.github.io/BEAR.QueryRepository/schemas/context/manual_store.json",
"title": "manual_store",
"description": "Open: an application-initiated (manual) store of a resource.",
"type": "object",
"required": ["uri"],
"properties": {
"uri": { "type": "string" }
},
"additionalProperties": false
}
Loading
Loading