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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Insight

- Added `evolvephp/insight` as a public experimental diagnostic-batch collection foundation. The package consumes Core execution observations through the existing observation sink boundary, collects immutable bounded per-execution batches and keeps persistence, retention, redaction, dashboards, watchers, OpenTelemetry, Evolve Observe, runtime wiring and independent package release deferred.
- Added storage-neutral Insight batch snapshots, projection from finalized diagnostic batches, a minimal diagnostic batch store contract, an in-memory store for tests and short-lived local development, and a sink adapter that projects and stores accepted batches while keeping persistent storage and retention deferred.

### Repository

Expand Down
16 changes: 14 additions & 2 deletions packages/insight/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

`evolvephp/insight`

Diagnostic batch collection foundation for EvolvePHP 2.
Diagnostic batch collection and storage-projection foundation for EvolvePHP 2.

## Responsibility

Evolve Insight consumes safe Core execution observations and collects them into immutable, bounded diagnostic batches. This package provides the storage-neutral `DiagnosticBatchSink` contract, immutable `DiagnosticBatch` values and `DiagnosticBatchCollector`, which implements Core's `ObservationSink` boundary.

Insight also provides a storage-neutral projection boundary for finalized diagnostic batches. `DiagnosticBatchProjector` detaches a `DiagnosticBatch` into a primitive-only `DiagnosticBatchSnapshot` made of string-backed execution identity, execution kind, ordered `DiagnosticObservationSnapshot` values and the dropped observation count. Snapshots do not retain Core `Observation`, execution identifier, request, response, container, throwable or execution-scope objects.

Current bounded behavior:

- collection starts only after Core reports an execution start
Expand All @@ -18,6 +20,16 @@ Current bounded behavior:
- completed executions are forgotten before the finalized batch is handed to the configured sink
- sink failures propagate to the existing Core instrumentation boundary

Current storage behavior:

- `DiagnosticBatchStore` defines minimal save, exact execution-identifier lookup and newest-first bounded reads
- `InMemoryDiagnosticBatchStore` keeps snapshots in insertion order and returns newest batches first
- duplicate execution identifiers are rejected and never replace the original snapshot
- `StoringDiagnosticBatchSink` projects accepted batches and saves the detached snapshot through a configured store
- storage remains optional and unwired; installing Insight does not create storage automatically

The in-memory store is unbounded and suitable only for tests or short-lived local development. It is not a persistent runtime store.

## Requirements

PHP `^8.4`
Expand All @@ -34,7 +46,7 @@ https://github.com/josiahking/evolvephp

## Current Limitations

This package does not provide persistence, retention, pruning, redaction, filtering, sampling, dashboards, watchers, OpenTelemetry, Evolve Observe, trace propagation, runtime composition, automatic registration, storage adapters or production-ready diagnostics.
This package does not provide filesystem or database persistence, retention, pruning, redaction, rich diagnostic capture, filtering, sampling, dashboards, watchers, OpenTelemetry, Evolve Observe, trace propagation, runtime composition, automatic registration, persistent storage adapters or production-ready diagnostics.

## Licence

Expand Down
67 changes: 67 additions & 0 deletions packages/insight/src/Storage/DiagnosticBatchProjector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace Evolve\Insight\Storage;

use Evolve\Core\Execution\ProcessReuseDecision;
use Evolve\Core\Instrumentation\Observation;
use Evolve\Core\Instrumentation\ObservationOutcome;
use Evolve\Core\Instrumentation\ObservationType;
use Evolve\Insight\DiagnosticBatch;

final class DiagnosticBatchProjector
{
public function project(DiagnosticBatch $batch): DiagnosticBatchSnapshot
{
return new DiagnosticBatchSnapshot(
$batch->identifier()->value(),
$batch->kind()->value,
array_map(
fn (Observation $observation): DiagnosticObservationSnapshot => $this->projectObservation($observation),
$batch->observations(),
),
$batch->droppedObservationCount(),
);
}

private function projectObservation(Observation $observation): DiagnosticObservationSnapshot
{
return new DiagnosticObservationSnapshot(
$this->observationType($observation->type()),
$this->outcome($observation->outcome()),
$observation->errorType(),
$this->reuseDecision($observation->reuseDecision()),
);
}

private function observationType(ObservationType $type): string
{
return match ($type) {
ObservationType::ExecutionStarted => 'execution-started',
ObservationType::HandlerCompleted => 'handler-completed',
ObservationType::ScopeCloseStarted => 'scope-close-started',
ObservationType::ScopeCloseCompleted => 'scope-close-completed',
ObservationType::QuarantineRequired => 'quarantine-required',
ObservationType::ExecutionCompleted => 'execution-completed',
};
}

private function outcome(?ObservationOutcome $outcome): ?string
{
return match ($outcome) {
ObservationOutcome::Succeeded => 'succeeded',
ObservationOutcome::Failed => 'failed',
null => null,
};
}

private function reuseDecision(?ProcessReuseDecision $decision): ?string
{
return match ($decision) {
ProcessReuseDecision::Reusable => 'reusable',
ProcessReuseDecision::QuarantineRequired => 'quarantine-required',
null => null,
};
}
}
56 changes: 56 additions & 0 deletions packages/insight/src/Storage/DiagnosticBatchSnapshot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace Evolve\Insight\Storage;

final class DiagnosticBatchSnapshot
{
/**
* @var list<DiagnosticObservationSnapshot>
*/
private array $observations;

/**
* @param list<DiagnosticObservationSnapshot> $observations
*/
public function __construct(
private string $executionIdentifier,
private string $executionKind,
array $observations,
private int $droppedObservationCount,
) {
if ($this->executionIdentifier === '') {
throw new \InvalidArgumentException('Execution identifier must not be empty.');
}

if ($this->droppedObservationCount < 0) {
throw new \InvalidArgumentException('Dropped observation count must not be negative.');
}

$this->observations = $observations;
}

public function executionIdentifier(): string
{
return $this->executionIdentifier;
}

public function executionKind(): string
{
return $this->executionKind;
}

/**
* @return list<DiagnosticObservationSnapshot>
*/
public function observations(): array
{
return $this->observations;
}

public function droppedObservationCount(): int
{
return $this->droppedObservationCount;
}
}
17 changes: 17 additions & 0 deletions packages/insight/src/Storage/DiagnosticBatchStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Evolve\Insight\Storage;

interface DiagnosticBatchStore
{
public function save(DiagnosticBatchSnapshot $snapshot): void;

public function find(string $executionIdentifier): ?DiagnosticBatchSnapshot;

/**
* @return list<DiagnosticBatchSnapshot>
*/
public function latest(int $limit): array;
}
35 changes: 35 additions & 0 deletions packages/insight/src/Storage/DiagnosticObservationSnapshot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Evolve\Insight\Storage;

final readonly class DiagnosticObservationSnapshot
{
public function __construct(
private string $type,
private ?string $outcome,
private ?string $errorType,
private ?string $reuseDecision,
) {}

public function type(): string
{
return $this->type;
}

public function outcome(): ?string
{
return $this->outcome;
}

public function errorType(): ?string
{
return $this->errorType;
}

public function reuseDecision(): ?string
{
return $this->reuseDecision;
}
}
49 changes: 49 additions & 0 deletions packages/insight/src/Storage/InMemoryDiagnosticBatchStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Evolve\Insight\Storage;

final class InMemoryDiagnosticBatchStore implements DiagnosticBatchStore
{
/**
* @var array<string, DiagnosticBatchSnapshot>
*/
private array $snapshotsByIdentifier = array();

/**
* @var list<string>
*/
private array $insertionOrder = array();

public function save(DiagnosticBatchSnapshot $snapshot): void
{
$identifier = $snapshot->executionIdentifier();

if (isset($this->snapshotsByIdentifier[$identifier])) {
throw new \LogicException('Diagnostic batch snapshot already exists for execution identifier.');
}

$this->snapshotsByIdentifier[$identifier] = $snapshot;
$this->insertionOrder[] = $identifier;
}

public function find(string $executionIdentifier): ?DiagnosticBatchSnapshot
{
return $this->snapshotsByIdentifier[$executionIdentifier] ?? null;
}

public function latest(int $limit): array
{
if ($limit <= 0) {
throw new \InvalidArgumentException('Latest limit must be positive.');
}

$identifiers = array_slice(array_reverse($this->insertionOrder), 0, $limit);

return array_map(
fn (string $identifier): DiagnosticBatchSnapshot => $this->snapshotsByIdentifier[$identifier],
$identifiers,
);
}
}
21 changes: 21 additions & 0 deletions packages/insight/src/Storage/StoringDiagnosticBatchSink.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Evolve\Insight\Storage;

use Evolve\Insight\DiagnosticBatch;
use Evolve\Insight\DiagnosticBatchSink;

final readonly class StoringDiagnosticBatchSink implements DiagnosticBatchSink
{
public function __construct(
private DiagnosticBatchProjector $projector,
private DiagnosticBatchStore $store,
) {}

public function accept(DiagnosticBatch $batch): void
{
$this->store->save($this->projector->project($batch));
}
}
Loading