diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index aba1b9c2820..28e0139648f 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -88,6 +88,7 @@ jobs: uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: 8.4 + extensions: opentelemetry - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -119,6 +120,7 @@ jobs: uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: 8.4 + extensions: opentelemetry - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -148,6 +150,7 @@ jobs: with: php-version: 8.4 coverage: none + extensions: opentelemetry - name: Install Composer dependencies uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 @@ -174,6 +177,7 @@ jobs: with: php-version: 8.4 coverage: none + extensions: opentelemetry - name: Install Composer dependencies uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 @@ -217,7 +221,7 @@ jobs: uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: 8.4 - extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, sqlite3, zip + extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, sqlite3, zip, opentelemetry coverage: xdebug tools: pecl, composer @@ -479,7 +483,7 @@ jobs: id-token: write attestations: write env: - extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql, pgsql, sqlite3, zip + extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql, pgsql, sqlite3, zip, opentelemetry steps: - name: Harden Runner @@ -555,7 +559,7 @@ jobs: contents: write id-token: write env: - extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql, pgsql, sqlite3, zip + extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql, pgsql, sqlite3, zip, opentelemetry steps: - name: Install Cosign diff --git a/.github/workflows/php_dist.yml b/.github/workflows/php_dist.yml index 812f975baed..ece4218d2f3 100644 --- a/.github/workflows/php_dist.yml +++ b/.github/workflows/php_dist.yml @@ -38,7 +38,7 @@ jobs: # Maps tcp port 5432 on service container to the host - 5432:5432 env: - extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql, pgsql, sqlite3, zip + extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql, pgsql, sqlite3, zip, opentelemetry steps: - name: Harden Runner diff --git a/.github/workflows/php_tests.yml b/.github/workflows/php_tests.yml index 8a8f4f7a4db..7f5bc85b6ff 100644 --- a/.github/workflows/php_tests.yml +++ b/.github/workflows/php_tests.yml @@ -67,7 +67,7 @@ jobs: # Maps tcp port 5432 on service container to the host - 5432:5432 env: - extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql, pgsql, sqlite3, zip + extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql, pgsql, sqlite3, zip, opentelemetry steps: - name: Harden Runner diff --git a/app/Actions/Photo/Create.php b/app/Actions/Photo/Create.php index 7dfb0d5e0eb..ccb9f9b07ff 100644 --- a/app/Actions/Photo/Create.php +++ b/app/Actions/Photo/Create.php @@ -33,6 +33,7 @@ use App\Models\Photo; use App\Models\User; use App\Services\Image\FileExtensionService; +use App\Services\Telemetry\TraceService; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Pipeline\Pipeline; use LycheeVerify\Contract\VerifyInterface; @@ -42,6 +43,8 @@ class Create /** @var ImportParam the strategy parameters prepared and compiled by this class */ protected ImportParam $strategy_parameters; + private TraceService $trace; + public function __construct( ?ImportMode $import_mode, int $intended_owner_id, @@ -58,6 +61,7 @@ public function __construct( preallocated_id: $preallocated_id, upload_trust_level: $upload_trust_level, ); + $this->trace = app(TraceService::class); } /** @@ -81,59 +85,63 @@ public function __construct( */ public function add(NativeLocalFile $source_file, ?AbstractAlbum $album, ?int $file_last_modified_time = null): Photo { - $this->checkQuota($source_file); - - /** @var InitDTO $init_dto */ - $init_dto = new InitDTO( - parameters: $this->strategy_parameters, - source_file: $source_file, - album: $album, - file_last_modified_time: $file_last_modified_time - ); - - $pre_pipes = [ - Init\DetectAndStoreRaw::class, - Init\AssertSupportedMedia::class, - Init\FetchLastModifiedTime::class, - Init\MayLoadFileMetadata::class, - Init\FindDuplicate::class, - ]; - - $init_dto = app(Pipeline::class) - ->send($init_dto) - ->through($pre_pipes) - ->thenReturn(); + return $this->trace->traceMethod('photo.create.add', function () use ($source_file, + $album, + $file_last_modified_time) { + $this->checkQuota($source_file); + + /** @var InitDTO $init_dto */ + $init_dto = new InitDTO( + parameters: $this->strategy_parameters, + source_file: $source_file, + album: $album, + file_last_modified_time: $file_last_modified_time + ); + + $pre_pipes = [ + Init\DetectAndStoreRaw::class, + Init\AssertSupportedMedia::class, + Init\FetchLastModifiedTime::class, + Init\MayLoadFileMetadata::class, + Init\FindDuplicate::class, + ]; + + $init_dto = app(Pipeline::class) + ->send($init_dto) + ->through($pre_pipes) + ->thenReturn(); - if ($init_dto->duplicate !== null) { - return $this->handleDuplicate($init_dto); - } + if ($init_dto->duplicate !== null) { + return $this->handleDuplicate($init_dto); + } - $post_pipes = [ - Init\InitParentAlbum::class, - Init\LoadFileMetadata::class, - Init\FindLivePartner::class, - ]; + $post_pipes = [ + Init\InitParentAlbum::class, + Init\LoadFileMetadata::class, + Init\FindLivePartner::class, + ]; - $init_dto = app(Pipeline::class) - ->send($init_dto) - ->through($post_pipes) - ->thenReturn(); + $init_dto = app(Pipeline::class) + ->send($init_dto) + ->through($post_pipes) + ->thenReturn(); - if ($init_dto->live_partner === null) { - return $this->handleStandalone($init_dto); - } + if ($init_dto->live_partner === null) { + return $this->handleStandalone($init_dto); + } - // livePartner !== null - $file_extension_service = app(FileExtensionService::class); - if ($file_extension_service->isSupportedVideo($source_file->getMimeType(), $source_file->getOriginalExtension())) { - return $this->handleVideoLivePartner($init_dto); - } + // livePartner !== null + $file_extension_service = app(FileExtensionService::class); + if ($file_extension_service->isSupportedVideo($source_file->getMimeType(), $source_file->getOriginalExtension())) { + return $this->handleVideoLivePartner($init_dto); + } - if ($file_extension_service->isSupportedImage($source_file->getPath(), $source_file->getMimeType(), $source_file->getOriginalExtension())) { - return $this->handlePhotoLivePartner($init_dto); - } + if ($file_extension_service->isSupportedImage($source_file->getPath(), $source_file->getMimeType(), $source_file->getOriginalExtension())) { + return $this->handlePhotoLivePartner($init_dto); + } - throw new LycheeLogicException('Pipe system for importing video failed'); + throw new LycheeLogicException('Pipe system for importing video failed'); + }); } /** @@ -176,41 +184,43 @@ private function handleDuplicate(InitDTO $init_dto): Photo private function handleStandalone(InitDTO $init_dto): Photo { - $dto = StandaloneDTO::ofInit($init_dto); - - $pipes = [ - Standalone\FixTimeStamps::class, - Standalone\InitNamingStrategy::class, - Standalone\ApplyUserProvidedMetadata::class, - Shared\HydrateMetadata::class, - Shared\SetHighlighted::class, - Shared\SetOwnership::class, - Standalone\SetOriginalChecksum::class, - Standalone\FetchSourceImage::class, - Standalone\ExtractGoogleMotionPictures::class, - Standalone\PlacePhoto::class, - Standalone\PlaceGoogleMotionVideo::class, - Standalone\SetChecksum::class, - Standalone\AutoRenamer::class, - Shared\SetUploadValidated::class, - Shared\Save::class, - Shared\SetParent::class, - Shared\SaveStatistics::class, - Standalone\CreateOriginalSizeVariant::class, - Standalone\CreateRawSizeVariant::class, - Standalone\CreateSizeVariants::class, - Standalone\ApplyWatermark::class, - Standalone\EncodePlaceholder::class, - Standalone\ReplaceOriginalWithBackup::class, - Shared\UploadSizeVariantsToS3::class, - Shared\GeodecodeLocation::class, - Shared\ExtractColourPalette::class, - Shared\NotifyAlbums::class, - Standalone\AutoScanFacesOnUpload::class, - Standalone\AutoScanNsfwOnUpload::class, - ]; - - return $this->executePipeOnDTO($pipes, $dto)->getPhoto(); + return $this->trace->traceMethod('photo.handle_standalone', function () use ($init_dto) { + $dto = StandaloneDTO::ofInit($init_dto); + + $pipes = [ + Standalone\FixTimeStamps::class, + Standalone\InitNamingStrategy::class, + Standalone\ApplyUserProvidedMetadata::class, + Shared\HydrateMetadata::class, + Shared\SetHighlighted::class, + Shared\SetOwnership::class, + Standalone\SetOriginalChecksum::class, + Standalone\FetchSourceImage::class, + Standalone\ExtractGoogleMotionPictures::class, + Standalone\PlacePhoto::class, + Standalone\PlaceGoogleMotionVideo::class, + Standalone\SetChecksum::class, + Standalone\AutoRenamer::class, + Shared\SetUploadValidated::class, + Shared\Save::class, + Shared\SetParent::class, + Shared\SaveStatistics::class, + Standalone\CreateOriginalSizeVariant::class, + Standalone\CreateRawSizeVariant::class, + Standalone\CreateSizeVariants::class, + Standalone\ApplyWatermark::class, + Standalone\EncodePlaceholder::class, + Standalone\ReplaceOriginalWithBackup::class, + Shared\UploadSizeVariantsToS3::class, + Shared\GeodecodeLocation::class, + Shared\ExtractColourPalette::class, + Shared\NotifyAlbums::class, + Standalone\AutoScanFacesOnUpload::class, + Standalone\AutoScanNsfwOnUpload::class, + ]; + + return $this->executePipeOnDTO($pipes, $dto)->getPhoto(); + }); } private function handleVideoLivePartner(InitDTO $init_dto): Photo @@ -378,4 +388,4 @@ private function checkQuota(NativeLocalFile $source_file): void throw new QuotaExceededException(); } } -} \ No newline at end of file +} diff --git a/app/Actions/Photo/Pipes/Init/AbstractInitPipe.php b/app/Actions/Photo/Pipes/Init/AbstractInitPipe.php new file mode 100644 index 00000000000..2f96b914693 --- /dev/null +++ b/app/Actions/Photo/Pipes/Init/AbstractInitPipe.php @@ -0,0 +1,33 @@ +traceMethod($this->getSpanName(), fn () => $this->execute($state, $next)); + } + + abstract protected function getSpanName(): string; + + abstract protected function execute(InitDTO $state, \Closure $next): InitDTO; +} diff --git a/app/Actions/Photo/Pipes/Init/AssertSupportedMedia.php b/app/Actions/Photo/Pipes/Init/AssertSupportedMedia.php index bb6a4916098..38bc19eccee 100644 --- a/app/Actions/Photo/Pipes/Init/AssertSupportedMedia.php +++ b/app/Actions/Photo/Pipes/Init/AssertSupportedMedia.php @@ -8,7 +8,6 @@ namespace App\Actions\Photo\Pipes\Init; -use App\Contracts\PhotoCreate\InitPipe; use App\DTO\PhotoCreate\InitDTO; use App\Exceptions\MediaFileOperationException; use App\Exceptions\MediaFileUnsupportedException; @@ -17,7 +16,7 @@ /** * Assert whether we support said file. */ -class AssertSupportedMedia implements InitPipe +class AssertSupportedMedia extends AbstractInitPipe { public function __construct( private FileExtensionService $file_extension_service, @@ -30,7 +29,7 @@ public function __construct( * @throws MediaFileUnsupportedException * @throws MediaFileOperationException */ - public function handle(InitDTO $state, \Closure $next): InitDTO + protected function execute(InitDTO $state, \Closure $next): InitDTO { $this->file_extension_service->assertIsSupportedMediaOrAcceptedRaw( $state->source_file->getPath(), @@ -40,4 +39,9 @@ public function handle(InitDTO $state, \Closure $next): InitDTO return $next($state); } -} \ No newline at end of file + + protected function getSpanName(): string + { + return 'photo.assert_supported_media'; + } +} diff --git a/app/Actions/Photo/Pipes/Init/DetectAndStoreRaw.php b/app/Actions/Photo/Pipes/Init/DetectAndStoreRaw.php index 4746721b617..37e86fb4a08 100644 --- a/app/Actions/Photo/Pipes/Init/DetectAndStoreRaw.php +++ b/app/Actions/Photo/Pipes/Init/DetectAndStoreRaw.php @@ -9,7 +9,6 @@ namespace App\Actions\Photo\Pipes\Init; use App\Actions\Photo\Convert\RawToJpeg; -use App\Contracts\PhotoCreate\InitPipe; use App\DTO\PhotoCreate\InitDTO; use App\Exceptions\CannotConvertMediaFileException; use App\Exceptions\Handler; @@ -29,7 +28,7 @@ * by the existing flow), and log a warning. * - If the file is NOT a RAW format: pass through unchanged. */ -class DetectAndStoreRaw implements InitPipe +class DetectAndStoreRaw extends AbstractInitPipe { /** * @param InitDTO $state @@ -37,7 +36,7 @@ class DetectAndStoreRaw implements InitPipe * * @return InitDTO */ - public function handle(InitDTO $state, \Closure $next): InitDTO + protected function execute(InitDTO $state, \Closure $next): InitDTO { $ext = strtolower($state->source_file->getOriginalExtension()); if (!str_starts_with($ext, '.')) { @@ -69,4 +68,9 @@ public function handle(InitDTO $state, \Closure $next): InitDTO return $next($state); } + + protected function getSpanName(): string + { + return 'photo.detect_and_store_raw'; + } } diff --git a/app/Actions/Photo/Pipes/Init/FetchLastModifiedTime.php b/app/Actions/Photo/Pipes/Init/FetchLastModifiedTime.php index 6e4e6475235..1592d1d3af2 100644 --- a/app/Actions/Photo/Pipes/Init/FetchLastModifiedTime.php +++ b/app/Actions/Photo/Pipes/Init/FetchLastModifiedTime.php @@ -8,18 +8,17 @@ namespace App\Actions\Photo\Pipes\Init; -use App\Contracts\PhotoCreate\InitPipe; use App\DTO\PhotoCreate\InitDTO; /** * Set file_last_modified_time if null. */ -class FetchLastModifiedTime implements InitPipe +class FetchLastModifiedTime extends AbstractInitPipe { /** * {@inheritDoc} */ - public function handle(InitDTO $state, \Closure $next): InitDTO + protected function execute(InitDTO $state, \Closure $next): InitDTO { if ($state->file_last_modified_time === null) { $state->file_last_modified_time ??= $state->source_file->lastModified(); @@ -27,5 +26,10 @@ public function handle(InitDTO $state, \Closure $next): InitDTO return $next($state); } + + protected function getSpanName(): string + { + return 'photo.fetch_last_modified_time'; + } } diff --git a/app/Actions/Photo/Pipes/Init/FindDuplicate.php b/app/Actions/Photo/Pipes/Init/FindDuplicate.php index 5585256a187..b49c1cdf99a 100644 --- a/app/Actions/Photo/Pipes/Init/FindDuplicate.php +++ b/app/Actions/Photo/Pipes/Init/FindDuplicate.php @@ -8,7 +8,6 @@ namespace App\Actions\Photo\Pipes\Init; -use App\Contracts\PhotoCreate\InitPipe; use App\DTO\PhotoCreate\InitDTO; use App\Image\StreamStat; use App\Models\Photo; @@ -16,12 +15,12 @@ /** * Look for duplicates of the file in the database. */ -class FindDuplicate implements InitPipe +class FindDuplicate extends AbstractInitPipe { /** * {@inheritDoc} */ - public function handle(InitDTO $state, \Closure $next): InitDTO + protected function execute(InitDTO $state, \Closure $next): InitDTO { $checksum = StreamStat::createFromLocalFile($state->source_file)->checksum; @@ -33,5 +32,10 @@ public function handle(InitDTO $state, \Closure $next): InitDTO return $next($state); } + + protected function getSpanName(): string + { + return 'photo.find_duplicate'; + } } diff --git a/app/Actions/Photo/Pipes/Init/FindLivePartner.php b/app/Actions/Photo/Pipes/Init/FindLivePartner.php index 7cb4ee434bd..e54962ce988 100644 --- a/app/Actions/Photo/Pipes/Init/FindLivePartner.php +++ b/app/Actions/Photo/Pipes/Init/FindLivePartner.php @@ -9,7 +9,6 @@ namespace App\Actions\Photo\Pipes\Init; use App\Constants\PhotoAlbum as PA; -use App\Contracts\PhotoCreate\InitPipe; use App\DTO\PhotoCreate\InitDTO; use App\Exceptions\Internal\IllegalOrderOfOperationException; use App\Exceptions\Internal\LycheeAssertionError; @@ -19,7 +18,7 @@ /** * Try to link live photo components together. */ -class FindLivePartner implements InitPipe +class FindLivePartner extends AbstractInitPipe { public function __construct( private FileExtensionService $file_extension_service, @@ -29,7 +28,7 @@ public function __construct( /** * {@inheritDoc} */ - public function handle(InitDTO $state, \Closure $next): InitDTO + protected function execute(InitDTO $state, \Closure $next): InitDTO { try { // find a potential partner which has the same content id @@ -60,5 +59,10 @@ public function handle(InitDTO $state, \Closure $next): InitDTO } // @codeCoverageIgnoreEnd } + + protected function getSpanName(): string + { + return 'photo.find_live_partner'; + } } diff --git a/app/Actions/Photo/Pipes/Init/InitParentAlbum.php b/app/Actions/Photo/Pipes/Init/InitParentAlbum.php index 28f4c17c274..6446641646b 100644 --- a/app/Actions/Photo/Pipes/Init/InitParentAlbum.php +++ b/app/Actions/Photo/Pipes/Init/InitParentAlbum.php @@ -8,7 +8,6 @@ namespace App\Actions\Photo\Pipes\Init; -use App\Contracts\PhotoCreate\InitPipe; use App\DTO\PhotoCreate\InitDTO; use App\Exceptions\InvalidPropertyException; use App\Models\Album; @@ -18,14 +17,14 @@ /** * Init album. */ -class InitParentAlbum implements InitPipe +class InitParentAlbum extends AbstractInitPipe { /** * {@inheritDoc} * * @throws InvalidPropertyException */ - public function handle(InitDTO $state, \Closure $next): InitDTO + protected function execute(InitDTO $state, \Closure $next): InitDTO { if ($state->album === null || $state->album instanceof Album) { return $next($state); @@ -43,4 +42,9 @@ public function handle(InitDTO $state, \Closure $next): InitDTO throw new InvalidPropertyException('The given parent album does not support uploading'); } -} \ No newline at end of file + + protected function getSpanName(): string + { + return 'photo.init_parent_album'; + } +} diff --git a/app/Actions/Photo/Pipes/Init/LoadFileMetadata.php b/app/Actions/Photo/Pipes/Init/LoadFileMetadata.php index 92214e78d80..876178300c2 100644 --- a/app/Actions/Photo/Pipes/Init/LoadFileMetadata.php +++ b/app/Actions/Photo/Pipes/Init/LoadFileMetadata.php @@ -8,7 +8,6 @@ namespace App\Actions\Photo\Pipes\Init; -use App\Contracts\PhotoCreate\InitPipe; use App\DTO\PhotoCreate\InitDTO; use App\Exceptions\InvalidPropertyException; use App\Metadata\Extractor; @@ -16,14 +15,14 @@ /** * Load metadata from the file. */ -class LoadFileMetadata implements InitPipe +class LoadFileMetadata extends AbstractInitPipe { /** * {@inheritDoc} * * @throws InvalidPropertyException */ - public function handle(InitDTO $state, \Closure $next): InitDTO + protected function execute(InitDTO $state, \Closure $next): InitDTO { if ($state->exif_info !== null) { // Metadata already loaded @@ -39,7 +38,7 @@ public function handle(InitDTO $state, \Closure $next): InitDTO // Use basename of the original upload for the title, not the converted file if ( $state->exif_info->title === null || - $state->exif_info->title === '' + $state->exif_info->title === '' ) { $title_source = $state->raw_source_file ?? $state->source_file; $state->exif_info->title = mb_substr($title_source->getOriginalBasename(), 0, 100, 'UTF-8'); @@ -47,5 +46,10 @@ public function handle(InitDTO $state, \Closure $next): InitDTO return $next($state); } + + protected function getSpanName(): string + { + return 'photo.load_file_metadata'; + } } diff --git a/app/Actions/Photo/Pipes/Init/MayLoadFileMetadata.php b/app/Actions/Photo/Pipes/Init/MayLoadFileMetadata.php index 0060f1a99fa..ab2e4e87043 100644 --- a/app/Actions/Photo/Pipes/Init/MayLoadFileMetadata.php +++ b/app/Actions/Photo/Pipes/Init/MayLoadFileMetadata.php @@ -8,21 +8,20 @@ namespace App\Actions\Photo\Pipes\Init; -use App\Contracts\PhotoCreate\InitPipe; use App\DTO\PhotoCreate\InitDTO; use App\Exceptions\InvalidPropertyException; /** * Load metadata from the file. */ -class MayLoadFileMetadata extends LoadFileMetadata implements InitPipe +class MayLoadFileMetadata extends LoadFileMetadata { /** * {@inheritDoc} * * @throws InvalidPropertyException */ - public function handle(InitDTO $state, \Closure $next): InitDTO + protected function execute(InitDTO $state, \Closure $next): InitDTO { if ($state->import_mode->shall_resync_metadata) { // Load the metadata from the file @@ -31,5 +30,10 @@ public function handle(InitDTO $state, \Closure $next): InitDTO return $next($state); } + + protected function getSpanName(): string + { + return 'photo.may_load_file_metadata'; + } } diff --git a/app/Actions/Photo/Pipes/Standalone/AbstractStandalonePipe.php b/app/Actions/Photo/Pipes/Standalone/AbstractStandalonePipe.php new file mode 100644 index 00000000000..543a2a80e9a --- /dev/null +++ b/app/Actions/Photo/Pipes/Standalone/AbstractStandalonePipe.php @@ -0,0 +1,33 @@ +traceMethod($this->getSpanName(), fn () => $this->execute($state, $next)); + } + + abstract protected function getSpanName(): string; + + abstract protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO; +} diff --git a/app/Actions/Photo/Pipes/Standalone/ApplyUserProvidedMetadata.php b/app/Actions/Photo/Pipes/Standalone/ApplyUserProvidedMetadata.php index 33a34202e53..5f44516d8a0 100644 --- a/app/Actions/Photo/Pipes/Standalone/ApplyUserProvidedMetadata.php +++ b/app/Actions/Photo/Pipes/Standalone/ApplyUserProvidedMetadata.php @@ -8,7 +8,6 @@ namespace App\Actions\Photo\Pipes\Standalone; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\PhotoCreate\StandaloneDTO; /** @@ -23,9 +22,9 @@ * For duplicate uploads this pipe never runs; the duplicate keeps its existing * title and description. */ -class ApplyUserProvidedMetadata implements StandalonePipe +class ApplyUserProvidedMetadata extends AbstractStandalonePipe { - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { if ($state->title !== null) { $state->photo->title = $state->title; @@ -37,4 +36,9 @@ public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO return $next($state); } + + protected function getSpanName(): string + { + return 'photo.apply_user_provided_metadata'; + } } diff --git a/app/Actions/Photo/Pipes/Standalone/ApplyWatermark.php b/app/Actions/Photo/Pipes/Standalone/ApplyWatermark.php index ae619a5afc5..160bc527d67 100644 --- a/app/Actions/Photo/Pipes/Standalone/ApplyWatermark.php +++ b/app/Actions/Photo/Pipes/Standalone/ApplyWatermark.php @@ -8,7 +8,6 @@ namespace App\Actions\Photo\Pipes\Standalone; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\PhotoCreate\StandaloneDTO; use App\Enum\SizeVariantType; use App\Exceptions\Handler; @@ -19,7 +18,7 @@ /** * We apply watermarks on pictures at upload time. */ -class ApplyWatermark implements StandalonePipe +class ApplyWatermark extends AbstractStandalonePipe { public function __construct( protected readonly ConfigManager $config_manager, @@ -27,7 +26,7 @@ public function __construct( ) { } - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { // Skip if user explicitly opted out if ($state->apply_watermark === false) { @@ -68,4 +67,9 @@ public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO return $next($state); } + + protected function getSpanName(): string + { + return 'photo.apply_watermark'; + } } diff --git a/app/Actions/Photo/Pipes/Standalone/AutoRenamer.php b/app/Actions/Photo/Pipes/Standalone/AutoRenamer.php index 4056066a0c8..1512f57db08 100644 --- a/app/Actions/Photo/Pipes/Standalone/AutoRenamer.php +++ b/app/Actions/Photo/Pipes/Standalone/AutoRenamer.php @@ -8,9 +8,9 @@ namespace App\Actions\Photo\Pipes\Standalone; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\PhotoCreate\StandaloneDTO; use App\Metadata\Renamer\PhotoRenamer; +use Illuminate\Support\Facades\Log; /** * Apply renaming rules to the photo title. @@ -20,18 +20,22 @@ * * Maybe also consider whether Renaming should be applied at upload time. */ -class AutoRenamer implements StandalonePipe +class AutoRenamer extends AbstractStandalonePipe { - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { // Skip if the caller explicitly provided a title at upload time (FR-041-06). // User-supplied titles take precedence and must not be overwritten by renamer rules. if ($state->title !== null) { + Log::debug('Photo has a title, we don\'t rename it, skipping'); + return $next($state); } // Skip if not enabled. if (!$state->shall_rename_photo_title) { + Log::debug('renaming not necessary, skipping'); + return $next($state); } @@ -42,4 +46,9 @@ public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO return $next($state); } -} \ No newline at end of file + + protected function getSpanName(): string + { + return 'photo.auto_renamer'; + } +} diff --git a/app/Actions/Photo/Pipes/Standalone/AutoScanFacesOnUpload.php b/app/Actions/Photo/Pipes/Standalone/AutoScanFacesOnUpload.php index 06abf969ffa..7ebd2e7d6a5 100644 --- a/app/Actions/Photo/Pipes/Standalone/AutoScanFacesOnUpload.php +++ b/app/Actions/Photo/Pipes/Standalone/AutoScanFacesOnUpload.php @@ -8,7 +8,6 @@ namespace App\Actions\Photo\Pipes\Standalone; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\PhotoCreate\StandaloneDTO; use App\Jobs\DispatchFaceScanJob; use App\Repositories\ConfigManager; @@ -18,14 +17,14 @@ * Automatically trigger face scanning when a photo is uploaded, * if the AI Vision face scanning feature is enabled. */ -class AutoScanFacesOnUpload implements StandalonePipe +class AutoScanFacesOnUpload extends AbstractStandalonePipe { public function __construct( protected readonly ConfigManager $config_manager, ) { } - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { // Process through the rest of the pipeline first $state = $next($state); @@ -51,4 +50,9 @@ public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO return $state; } + + protected function getSpanName(): string + { + return 'photo.auto_scan_faces_on_upload'; + } } diff --git a/app/Actions/Photo/Pipes/Standalone/AutoScanNsfwOnUpload.php b/app/Actions/Photo/Pipes/Standalone/AutoScanNsfwOnUpload.php index 6166982cfc4..3c973aef892 100644 --- a/app/Actions/Photo/Pipes/Standalone/AutoScanNsfwOnUpload.php +++ b/app/Actions/Photo/Pipes/Standalone/AutoScanNsfwOnUpload.php @@ -8,21 +8,20 @@ namespace App\Actions\Photo\Pipes\Standalone; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\PhotoCreate\StandaloneDTO; use App\Enum\UserUploadTrustLevel; use App\Jobs\DispatchNsfwScanJob; use App\Repositories\ConfigManager; use Illuminate\Support\Facades\Log; -class AutoScanNsfwOnUpload implements StandalonePipe +class AutoScanNsfwOnUpload extends AbstractStandalonePipe { public function __construct( protected readonly ConfigManager $config_manager, ) { } - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { // Step 1: Snapshot the uploader's trust level on the in-memory model. $state->photo->upload_trust_level = $state->upload_trust_level; @@ -79,4 +78,9 @@ private function shouldHideOnScan(UserUploadTrustLevel $trust_level): bool default => false, // CHECK users are already hidden via SetUploadValidated }; } + + protected function getSpanName(): string + { + return 'photo.auto_scan_nsfw_on_upload'; + } } diff --git a/app/Actions/Photo/Pipes/Standalone/CreateOriginalSizeVariant.php b/app/Actions/Photo/Pipes/Standalone/CreateOriginalSizeVariant.php index 2f7757e6450..d79a9b17404 100644 --- a/app/Actions/Photo/Pipes/Standalone/CreateOriginalSizeVariant.php +++ b/app/Actions/Photo/Pipes/Standalone/CreateOriginalSizeVariant.php @@ -8,15 +8,14 @@ namespace App\Actions\Photo\Pipes\Standalone; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\ImageDimension; use App\DTO\PhotoCreate\StandaloneDTO; use App\Enum\SizeVariantType; use App\Image\StreamStat; -class CreateOriginalSizeVariant implements StandalonePipe +class CreateOriginalSizeVariant extends AbstractStandalonePipe { - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { /** @var StreamStat $stat */ $stat = $state->stream_stat; @@ -37,4 +36,9 @@ public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO return $next($state); } -} \ No newline at end of file + + protected function getSpanName(): string + { + return 'photo.create_original_size_variant'; + } +} diff --git a/app/Actions/Photo/Pipes/Standalone/CreateRawSizeVariant.php b/app/Actions/Photo/Pipes/Standalone/CreateRawSizeVariant.php index a6bea65cf9f..0eae0b7b0b2 100644 --- a/app/Actions/Photo/Pipes/Standalone/CreateRawSizeVariant.php +++ b/app/Actions/Photo/Pipes/Standalone/CreateRawSizeVariant.php @@ -8,7 +8,6 @@ namespace App\Actions\Photo\Pipes\Standalone; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\CreateSizeVariantFlags; use App\DTO\ImageDimension; use App\DTO\PhotoCreate\StandaloneDTO; @@ -22,9 +21,9 @@ * If so, it copies the original (untouched) file to storage and creates * the RAW row in `size_variants`. */ -class CreateRawSizeVariant implements StandalonePipe +class CreateRawSizeVariant extends AbstractStandalonePipe { - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { if ($state->raw_source_file === null) { return $next($state); @@ -75,4 +74,9 @@ public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO return $next($state); } + + protected function getSpanName(): string + { + return 'photo.create_raw_size_variant'; + } } diff --git a/app/Actions/Photo/Pipes/Standalone/CreateSizeVariants.php b/app/Actions/Photo/Pipes/Standalone/CreateSizeVariants.php index b99ddbe0e7e..00438359d42 100644 --- a/app/Actions/Photo/Pipes/Standalone/CreateSizeVariants.php +++ b/app/Actions/Photo/Pipes/Standalone/CreateSizeVariants.php @@ -9,13 +9,12 @@ namespace App\Actions\Photo\Pipes\Standalone; use App\Contracts\Models\SizeVariantFactory; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\PhotoCreate\StandaloneDTO; use App\Exceptions\Handler; -class CreateSizeVariants implements StandalonePipe +class CreateSizeVariants extends AbstractStandalonePipe { - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { // Create remaining size variants if we were able to successfully // extract a reference image @@ -37,4 +36,9 @@ public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO return $next($state); } -} \ No newline at end of file + + protected function getSpanName(): string + { + return 'photo.create_size_variants'; + } +} diff --git a/app/Actions/Photo/Pipes/Standalone/EncodePlaceholder.php b/app/Actions/Photo/Pipes/Standalone/EncodePlaceholder.php index 7997b042a93..2d1da23bc65 100644 --- a/app/Actions/Photo/Pipes/Standalone/EncodePlaceholder.php +++ b/app/Actions/Photo/Pipes/Standalone/EncodePlaceholder.php @@ -8,14 +8,13 @@ namespace App\Actions\Photo\Pipes\Standalone; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\PhotoCreate\StandaloneDTO; use App\Exceptions\Handler; use App\Image\PlaceholderEncoder; -class EncodePlaceholder implements StandalonePipe +class EncodePlaceholder extends AbstractStandalonePipe { - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { try { $placeholder_encoder = new PlaceholderEncoder(); @@ -35,4 +34,9 @@ public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO return $next($state); } + + protected function getSpanName(): string + { + return 'photo.encode_placeholder'; + } } diff --git a/app/Actions/Photo/Pipes/Standalone/ExtractGoogleMotionPictures.php b/app/Actions/Photo/Pipes/Standalone/ExtractGoogleMotionPictures.php index e1f63e75fe0..0ec97951e24 100644 --- a/app/Actions/Photo/Pipes/Standalone/ExtractGoogleMotionPictures.php +++ b/app/Actions/Photo/Pipes/Standalone/ExtractGoogleMotionPictures.php @@ -8,15 +8,14 @@ namespace App\Actions\Photo\Pipes\Standalone; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\PhotoCreate\StandaloneDTO; use App\Exceptions\Handler; use App\Image\Files\TemporaryLocalFile; use App\Image\Handlers\GoogleMotionPictureHandler; -class ExtractGoogleMotionPictures implements StandalonePipe +class ExtractGoogleMotionPictures extends AbstractStandalonePipe { - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { if ($state->exif_info->micro_video_offset === 0) { return $next($state); @@ -38,4 +37,9 @@ public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO return $next($state); } + + protected function getSpanName(): string + { + return 'photo.set_original_checksum'; + } } diff --git a/app/Actions/Photo/Pipes/Standalone/FetchSourceImage.php b/app/Actions/Photo/Pipes/Standalone/FetchSourceImage.php index f143598ab5f..eb643e22256 100644 --- a/app/Actions/Photo/Pipes/Standalone/FetchSourceImage.php +++ b/app/Actions/Photo/Pipes/Standalone/FetchSourceImage.php @@ -8,15 +8,14 @@ namespace App\Actions\Photo\Pipes\Standalone; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\PhotoCreate\StandaloneDTO; use App\Exceptions\Handler; use App\Image\Handlers\ImageHandler; use App\Image\Handlers\VideoHandler; -class FetchSourceImage implements StandalonePipe +class FetchSourceImage extends AbstractStandalonePipe { - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { try { if ($state->photo->isVideo()) { @@ -42,5 +41,10 @@ public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO return $next($state); } + + protected function getSpanName(): string + { + return 'photo.fetch_image_source'; + } } diff --git a/app/Actions/Photo/Pipes/Standalone/FixTimeStamps.php b/app/Actions/Photo/Pipes/Standalone/FixTimeStamps.php index e22411c0f29..3a478ff6f7e 100644 --- a/app/Actions/Photo/Pipes/Standalone/FixTimeStamps.php +++ b/app/Actions/Photo/Pipes/Standalone/FixTimeStamps.php @@ -8,18 +8,22 @@ namespace App\Actions\Photo\Pipes\Standalone; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\PhotoCreate\StandaloneDTO; /** * Set the timestamps of the creation and updated_at time. */ -class FixTimeStamps implements StandalonePipe +class FixTimeStamps extends AbstractStandalonePipe { - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { $state->photo->updateTimestamps(); return $next($state); } + + protected function getSpanName(): string + { + return 'photo.fix_timestamps'; + } } diff --git a/app/Actions/Photo/Pipes/Standalone/InitNamingStrategy.php b/app/Actions/Photo/Pipes/Standalone/InitNamingStrategy.php index debba9dcae1..77d5bf67a1d 100644 --- a/app/Actions/Photo/Pipes/Standalone/InitNamingStrategy.php +++ b/app/Actions/Photo/Pipes/Standalone/InitNamingStrategy.php @@ -9,13 +9,14 @@ namespace App\Actions\Photo\Pipes\Standalone; use App\Contracts\Models\AbstractSizeVariantNamingStrategy; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\PhotoCreate\StandaloneDTO; +use Illuminate\Support\Facades\Log; -class InitNamingStrategy implements StandalonePipe +class InitNamingStrategy extends AbstractStandalonePipe { - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { + Log::debug('Executing InitNamingStrategy pipe'); $state->naming_strategy = resolve(AbstractSizeVariantNamingStrategy::class); $state->naming_strategy->setPhoto($state->photo); $state->naming_strategy->setExtension( @@ -24,4 +25,9 @@ public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO return $next($state); } + + protected function getSpanName(): string + { + return 'photo.init_naming_strategy'; + } } diff --git a/app/Actions/Photo/Pipes/Standalone/PlaceGoogleMotionVideo.php b/app/Actions/Photo/Pipes/Standalone/PlaceGoogleMotionVideo.php index d7c383b6964..efd802cf890 100644 --- a/app/Actions/Photo/Pipes/Standalone/PlaceGoogleMotionVideo.php +++ b/app/Actions/Photo/Pipes/Standalone/PlaceGoogleMotionVideo.php @@ -8,13 +8,12 @@ namespace App\Actions\Photo\Pipes\Standalone; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\PhotoCreate\StandaloneDTO; use App\Image\Files\FlysystemFile; -class PlaceGoogleMotionVideo implements StandalonePipe +class PlaceGoogleMotionVideo extends AbstractStandalonePipe { - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { // If we have a temporary video file from a Google Motion Picture, // we must move the preliminary extracted video file next to the @@ -35,4 +34,9 @@ public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO return $next($state); } + + protected function getSpanName(): string + { + return 'photo.place_google_motion_video'; + } } diff --git a/app/Actions/Photo/Pipes/Standalone/PlacePhoto.php b/app/Actions/Photo/Pipes/Standalone/PlacePhoto.php index 198ff26d194..1b9e26f4317 100644 --- a/app/Actions/Photo/Pipes/Standalone/PlacePhoto.php +++ b/app/Actions/Photo/Pipes/Standalone/PlacePhoto.php @@ -10,7 +10,6 @@ use App\Actions\Diagnostics\Pipes\Checks\BasicPermissionCheck; use App\Contracts\Image\StreamStats; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\CreateSizeVariantFlags; use App\DTO\PhotoCreate\StandaloneDTO; use App\Enum\SizeVariantType; @@ -20,14 +19,14 @@ use App\Image\StreamStat; use App\Repositories\ConfigManager; -class PlacePhoto implements StandalonePipe +class PlacePhoto extends AbstractStandalonePipe { public function __construct( protected readonly ConfigManager $config_manager, ) { } - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { // Create target file and symlink/copy/move source file to target. // If the import strategy requests to delete the source file @@ -147,4 +146,9 @@ private function backupOriginal(StandaloneDTO $state) $state->backup_file = $state->naming_strategy->createFile(SizeVariantType::ORIGINAL, new CreateSizeVariantFlags(is_backup: true)); $state->backup_file->write($state->source_file->read(), true); } + + protected function getSpanName(): string + { + return 'photo.place_photo'; + } } diff --git a/app/Actions/Photo/Pipes/Standalone/ReplaceOriginalWithBackup.php b/app/Actions/Photo/Pipes/Standalone/ReplaceOriginalWithBackup.php index ff734315f51..8e81753d91a 100644 --- a/app/Actions/Photo/Pipes/Standalone/ReplaceOriginalWithBackup.php +++ b/app/Actions/Photo/Pipes/Standalone/ReplaceOriginalWithBackup.php @@ -8,18 +8,17 @@ namespace App\Actions\Photo\Pipes\Standalone; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\PhotoCreate\StandaloneDTO; use App\Repositories\ConfigManager; -class ReplaceOriginalWithBackup implements StandalonePipe +class ReplaceOriginalWithBackup extends AbstractStandalonePipe { public function __construct( protected readonly ConfigManager $config_manager, ) { } - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { if ($state->backup_file === null) { return $next($state); @@ -34,4 +33,9 @@ public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO return $next($state); } + + protected function getSpanName(): string + { + return 'photo.replace_original_with_backup'; + } } diff --git a/app/Actions/Photo/Pipes/Standalone/SetChecksum.php b/app/Actions/Photo/Pipes/Standalone/SetChecksum.php index f19b2ef09ce..32594579e71 100644 --- a/app/Actions/Photo/Pipes/Standalone/SetChecksum.php +++ b/app/Actions/Photo/Pipes/Standalone/SetChecksum.php @@ -8,13 +8,12 @@ namespace App\Actions\Photo\Pipes\Standalone; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\PhotoCreate\StandaloneDTO; use App\Image\StreamStat; -class SetChecksum implements StandalonePipe +class SetChecksum extends AbstractStandalonePipe { - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { /** @var StreamStat $stat */ $stat = $state->stream_stat; @@ -26,4 +25,9 @@ public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO return $next($state); } -} \ No newline at end of file + + protected function getSpanName(): string + { + return 'photo.set_checksum'; + } +} diff --git a/app/Actions/Photo/Pipes/Standalone/SetOriginalChecksum.php b/app/Actions/Photo/Pipes/Standalone/SetOriginalChecksum.php index 875398beccf..6a75902403f 100644 --- a/app/Actions/Photo/Pipes/Standalone/SetOriginalChecksum.php +++ b/app/Actions/Photo/Pipes/Standalone/SetOriginalChecksum.php @@ -8,14 +8,16 @@ namespace App\Actions\Photo\Pipes\Standalone; -use App\Contracts\PhotoCreate\StandalonePipe; use App\DTO\PhotoCreate\StandaloneDTO; use App\Image\StreamStat; +use Illuminate\Support\Facades\Log; -class SetOriginalChecksum implements StandalonePipe +class SetOriginalChecksum extends AbstractStandalonePipe { - public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO + protected function execute(StandaloneDTO $state, \Closure $next): StandaloneDTO { + Log::debug('Computing original checksum'); + // Unfortunately, we must read the entire file once to create the // true, original checksum. // It does **not** suffice to use the stream statistics, when the @@ -32,4 +34,9 @@ public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO return $next($state); } + + protected function getSpanName(): string + { + return 'photo.set_original_checksum'; + } } diff --git a/app/Contracts/Telemetry/TraceServiceInterface.php b/app/Contracts/Telemetry/TraceServiceInterface.php new file mode 100644 index 00000000000..be4c9d11953 --- /dev/null +++ b/app/Contracts/Telemetry/TraceServiceInterface.php @@ -0,0 +1,18 @@ +meta(); - $file = new UploadedFile($request->uploaded_file_chunk()); + // Reset max memory usage before measuring + memory_reset_peak_usage(); - // Set up meta data if not already present - $meta->extension ??= '.' . pathinfo($meta->file_name, PATHINFO_EXTENSION); - $meta->uuid_name ??= strtr(base64_encode(random_bytes(12)), '+/', '-_') . $meta->extension; + return $this->trace->traceMethod('photo.upload', function () use ($request) { + $this->trace->addEventWithMemToCurrentSpan('Upload photo'); - $final = new NativeLocalFile(Storage::disk(FileSystem::IMAGE_UPLOAD)->path($meta->uuid_name)); - $final->append($file->read()); + $meta = $request->meta(); + $file = new UploadedFile($request->uploaded_file_chunk()); - if ($meta->chunk_number < $meta->total_chunks) { - // Not the last chunk - return $meta; - } + // Set up meta data if not already present + $meta->extension ??= '.' . pathinfo($meta->file_name, PATHINFO_EXTENSION); + $meta->uuid_name ??= strtr(base64_encode(random_bytes(12)), '+/', '-_') . $meta->extension; - // Last chunk — generate expected_id for non-zip uploads and store title/description. - $meta->stage = FileStatus::PROCESSING; - $meta->title = $request->title(); - $meta->description = $request->description(); + $final = new NativeLocalFile(Storage::disk(FileSystem::IMAGE_UPLOAD)->path($meta->uuid_name)); + $final->append($file->read()); - $is_zip = strtolower(pathinfo($meta->file_name, PATHINFO_EXTENSION)) === 'zip'; - if (!$is_zip) { - $id_factory = resolve(IdFactory::class); - $meta->expected_id = $id_factory->createRandomID(); - } + if ($meta->chunk_number < $meta->total_chunks) { + // Not the last chunk + return $meta; + } - return $this->process( - $request->verify(), - $request->configs(), - $final, - $request->album(), - $request->file_last_modified_time(), - $request->apply_watermark(), - $meta); + // Last chunk — generate expected_id for non-zip uploads and store title/description. + $meta->stage = FileStatus::PROCESSING; + $meta->title = $request->title(); + $meta->description = $request->description(); + + $is_zip = strtolower(pathinfo($meta->file_name, PATHINFO_EXTENSION)) === 'zip'; + if (!$is_zip) { + $id_factory = resolve(IdFactory::class); + $meta->expected_id = $id_factory->createRandomID(); + } + + $this->trace->addMemoryInfoToCurrentSpan(); + + return $this->process( + $request->verify(), + $request->configs(), + $final, + $request->album(), + $request->file_last_modified_time(), + $request->apply_watermark(), + $meta); + }); } private function process( @@ -115,34 +130,49 @@ private function process( ?bool $apply_watermark, UploadMetaResource $meta, ): UploadMetaResource { - $processable_file = new ProcessableJobFile( - $final->getOriginalExtension(), - $meta->file_name - ); - $processable_file->write($final->read()); - - $final->close(); - $final->delete(); - $processable_file->close(); - // End of work-around + return $this->trace->traceMethod('photo.process', function () use ($verify, + $config_manager, + $final, + $album, + $file_last_modified_time, + $apply_watermark, + $meta) { + $processable_file = new ProcessableJobFile( + $final->getOriginalExtension(), + $meta->file_name + ); + $processable_file->write($final->read()); + + $final->close(); + $final->delete(); + $processable_file->close(); + // End of work-around + + $is_zip = strtolower(pathinfo($meta->file_name, PATHINFO_EXTENSION)) === 'zip'; + $is_se = $verify->is_supporter(); + + if ($is_se && $config_manager->getValueAsBool('extract_zip_on_upload') && $is_zip) { + ExtractZip::dispatch($processable_file, $album?->get_id(), $file_last_modified_time); + // We return DONE no matter what: + // - if we are in sync mode, this will be executed after the job + // - if we are in async mode, the job will be executed later, but we need to notify the front-end we are clear. + $meta->stage = FileStatus::DONE; + + return $meta; + } - $is_zip = strtolower(pathinfo($meta->file_name, PATHINFO_EXTENSION)) === 'zip'; - $is_se = $verify->is_supporter(); + $this->trace->traceMethod('photo.dispatch', function () use ($processable_file, + $album, + $file_last_modified_time, + $apply_watermark, $meta): void { + ProcessImageJob::dispatch($processable_file, $album, $file_last_modified_time, $apply_watermark, $meta->expected_id, $meta->title, $meta->description); + $meta->stage = config('queue.default') === 'sync' ? FileStatus::DONE : FileStatus::READY; + }); - if ($is_se && $config_manager->getValueAsBool('extract_zip_on_upload') && $is_zip) { - ExtractZip::dispatch($processable_file, $album?->get_id(), $file_last_modified_time); - // We return DONE no matter what: - // - if we are in sync mode, this will be executed after the job - // - if we are in async mode, the job will be executed later, but we need to notify the front-end we are clear. - $meta->stage = FileStatus::DONE; + $this->trace->addEventWithMemToCurrentSpan('End of image processing'); return $meta; - } - - ProcessImageJob::dispatch($processable_file, $album, $file_last_modified_time, $apply_watermark, $meta->expected_id, $meta->title, $meta->description); - $meta->stage = config('queue.default') === 'sync' ? FileStatus::DONE : FileStatus::READY; - - return $meta; + }); } /** diff --git a/app/Jobs/ProcessImageJob.php b/app/Jobs/ProcessImageJob.php index 4be7f967d5b..52b3c3492f1 100644 --- a/app/Jobs/ProcessImageJob.php +++ b/app/Jobs/ProcessImageJob.php @@ -23,6 +23,7 @@ use App\Models\Photo; use App\Models\TagAlbum; use App\Repositories\ConfigManager; +use App\Services\Telemetry\TraceService; use App\SmartAlbums\BaseSmartAlbum; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -58,6 +59,8 @@ class ProcessImageJob implements ShouldQueue public ?string $title; public ?string $description; + private TraceService $trace; + /** * Create a new job instance. */ @@ -70,6 +73,7 @@ public function __construct( ?string $title = null, ?string $description = null, ) { + $this->trace = app(TraceService::class); $this->file_path = $file->getPath(); $this->original_base_name = $file->getOriginalBasename(); @@ -133,39 +137,43 @@ public function __construct( */ public function handle(AlbumFactory $album_factory): Photo { - $this->history->status = JobStatus::STARTED; - $this->history->save(); - Log::channel('jobs')->info($this->history->job); - - $copied_file = new TemporaryJobFile($this->file_path, $this->original_base_name); - - $config_manager = app(ConfigManager::class); - - // As the file has been uploaded, the (temporary) source file shall be deleted - $create = new Create( - import_mode: new ImportMode( - delete_imported: true, - skip_duplicates: $config_manager->getValueAsBool('skip_duplicates'), - shall_rename_photo_title: $config_manager->getValueAsBool('renamer_photo_title_enabled'), - ), - intended_owner_id: $this->user_id, - upload_trust_level: $this->upload_trust_level, - title: $this->title, - description: $this->description, - preallocated_id: $this->expected_id, - ); - - $album = null; - if ($this->album_id !== null) { - $album = $album_factory->findAbstractAlbumOrFail($this->album_id); - } - - $photo = $create->add($copied_file, $album, $this->file_last_modified_time); - - // Once the job has finished, set history status to 1. - $this->history->status = JobStatus::SUCCESS; - $this->history->save(); - - return $photo; + return $this->trace->traceMethod('photo.process-execution', function () use ($album_factory) { + $this->trace->addEventWithMemToCurrentSpan('Process image'); + + $this->history->status = JobStatus::STARTED; + $this->history->save(); + Log::channel('jobs')->info($this->history->job); + + $copied_file = new TemporaryJobFile($this->file_path, $this->original_base_name); + + $config_manager = app(ConfigManager::class); + + // As the file has been uploaded, the (temporary) source file shall be deleted + $create = new Create( + import_mode: new ImportMode( + delete_imported: true, + skip_duplicates: $config_manager->getValueAsBool('skip_duplicates'), + shall_rename_photo_title: $config_manager->getValueAsBool('renamer_photo_title_enabled'), + ), + intended_owner_id: $this->user_id, + upload_trust_level: $this->upload_trust_level, + title: $this->title, + description: $this->description, + preallocated_id: $this->expected_id, + ); + + $album = null; + if ($this->album_id !== null) { + $album = $album_factory->findAbstractAlbumOrFail($this->album_id); + } + + $photo = $create->add($copied_file, $album, $this->file_last_modified_time); + + // Once the job has finished, set history status to 1. + $this->history->status = JobStatus::SUCCESS; + $this->history->save(); + + return $photo; + }); } -} \ No newline at end of file +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f326c89322e..71d5e85cd81 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -36,6 +36,7 @@ use App\Policies\SettingsPolicy; use App\Repositories\ConfigManager; use App\Services\MoneyService; +use App\Services\Telemetry\TraceService; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Database\Connection; use Illuminate\Database\Eloquent\Model; @@ -104,6 +105,7 @@ class AppServiceProvider extends ServiceProvider GitTags::class => GitTags::class, MoneyService::class => MoneyService::class, + TraceService::class => TraceService::class, ]; /** diff --git a/app/Services/Telemetry/TraceService.php b/app/Services/Telemetry/TraceService.php new file mode 100644 index 00000000000..0cf1d909d2e --- /dev/null +++ b/app/Services/Telemetry/TraceService.php @@ -0,0 +1,73 @@ +setAttribute($name, $value); + } + + public function addMemoryInfoToCurrentSpan(): void + { + $this->addAttributeToCurrentSpan('lychee.memory.current', memory_get_usage(true)); + $this->addAttributeToCurrentSpan('lychee.memory.peak', memory_get_peak_usage(true)); + } + + public function addEventToCurrentSpan(string $name, array $attributes = []): void + { + Span::getCurrent()->addEvent($name, $attributes); + } + + public function addEventWithMemToCurrentSpan(string $eventName): void + { + $this->addEventToCurrentSpan($eventName, [ + 'lychee.memory.current' => memory_get_usage(true), + 'lychee.memory.peak' => memory_get_peak_usage(true), + ]); + } + + public function traceMethod(string $name, \Closure $callback): mixed + { + $tracer = Globals::tracerProvider()->getTracer('lychee'); + + $span = $tracer + ->spanBuilder($name) + ->startSpan(); + + if (!$span->isRecording()) { + return $callback(); + } + + $before = memory_get_usage(true); + + $scope = $span->activate(); + + try { + $result = $callback(); + + $span->setAttributes([ + 'lychee.memory.before' => $before, + 'lychee.memory.after' => memory_get_usage(true), + 'lychee.memory.peak' => memory_get_peak_usage(true), + ]); + + return $result; + } finally { + $scope->detach(); + $span->end(); + } + } +} + diff --git a/composer.json b/composer.json index 79ab51a1555..f5ece9c9cb0 100644 --- a/composer.json +++ b/composer.json @@ -77,6 +77,11 @@ "omnipay/mollie": "^5.5", "omnipay/stripe": "^3.2", "opcodesio/log-viewer": "dev-lycheeOrg", + "open-telemetry/exporter-otlp": "^1.4", + "open-telemetry/opentelemetry-auto-guzzle": "^1.4", + "open-telemetry/opentelemetry-auto-laravel": "^1.8", + "open-telemetry/opentelemetry-auto-pdo": "^0.5.0", + "open-telemetry/sdk": "^1.15", "paypal/paypal-server-sdk": "2.3.0", "php-ffmpeg/php-ffmpeg": "^1.0", "php-http/guzzle7-adapter": "^1.0", @@ -166,7 +171,8 @@ "sort-packages": true, "optimize-autoloader": true, "allow-plugins": { - "php-http/discovery": false + "php-http/discovery": false, + "tbachert/spi": true } }, "suggest": { diff --git a/composer.lock b/composer.lock index a2db1b918f3..3296a6b1f9c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ed3bb66f667998fee1f2507d7f5d1395", + "content-hash": "25deb6aac1e9efe85c0aa3ff1143e719", "packages": [ { "name": "apimatic/core", @@ -2175,6 +2175,50 @@ }, "time": "2025-04-15T13:24:32+00:00" }, + { + "name": "google/protobuf", + "version": "v5.35.1", + "source": { + "type": "git", + "url": "https://github.com/protocolbuffers/protobuf-php.git", + "reference": "55bb4a7d6739b5af0927b96213c1371a3afb7cfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/55bb4a7d6739b5af0927b96213c1371a3afb7cfb", + "reference": "55bb4a7d6739b5af0927b96213c1371a3afb7cfb", + "shasum": "" + }, + "require": { + "php": ">=8.2.0" + }, + "require-dev": { + "phpunit/phpunit": ">=11.5.0 <12.0.0" + }, + "suggest": { + "ext-bcmath": "Need to support JSON deserialization" + }, + "type": "library", + "autoload": { + "psr-4": { + "Google\\Protobuf\\": "src/Google/Protobuf", + "GPBMetadata\\Google\\Protobuf\\": "src/GPBMetadata/Google/Protobuf" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "proto library for PHP", + "homepage": "https://developers.google.com/protocol-buffers/", + "keywords": [ + "proto" + ], + "support": { + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v5.35.1" + }, + "time": "2026-06-11T21:19:23+00:00" + }, { "name": "graham-campbell/markdown", "version": "v16.1.0", @@ -5257,16 +5301,16 @@ }, { "name": "nesbot/carbon", - "version": "3.13.1", + "version": "3.13.2", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "2937ad3d1d2c506fd2bc97d571438a95641f44e2" + "reference": "a1c54919f5fff9800cd03c32bd01defd5a4061cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/2937ad3d1d2c506fd2bc97d571438a95641f44e2", - "reference": "2937ad3d1d2c506fd2bc97d571438a95641f44e2", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/a1c54919f5fff9800cd03c32bd01defd5a4061cb", + "reference": "a1c54919f5fff9800cd03c32bd01defd5a4061cb", "shasum": "" }, "require": { @@ -5358,7 +5402,7 @@ "type": "tidelift" } ], - "time": "2026-07-09T18:23:49+00:00" + "time": "2026-08-08T11:40:35+00:00" }, { "name": "nette/schema", @@ -5662,6 +5706,72 @@ ], "time": "2026-02-16T23:10:27+00:00" }, + { + "name": "nyholm/psr7-server", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7-server.git", + "reference": "4335801d851f554ca43fa6e7d2602141538854dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/4335801d851f554ca43fa6e7d2602141538854dc", + "reference": "4335801d851f554ca43fa6e7d2602141538854dc", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "nyholm/nsa": "^1.1", + "nyholm/psr7": "^1.3", + "phpunit/phpunit": "^7.0 || ^8.5 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Nyholm\\Psr7Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "Helper classes to handle PSR-7 server requests", + "homepage": "http://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7-server/issues", + "source": "https://github.com/Nyholm/psr7-server/tree/1.1.0" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2023-11-08T09:30:43+00:00" + }, { "name": "omnipay/common", "version": "v3.5.1", @@ -5922,197 +6032,806 @@ }, "autoload": { "psr-4": { - "Omnipay\\Stripe\\": "src/" + "Omnipay\\Stripe\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Adrian Macneil", + "email": "adrian@adrianmacneil.com" + }, + { + "name": "Omnipay Contributors", + "homepage": "https://github.com/thephpleague/omnipay-stripe/contributors" + } + ], + "description": "Stripe driver for the Omnipay payment processing library", + "homepage": "https://github.com/thephpleague/omnipay-stripe", + "keywords": [ + "gateway", + "merchant", + "omnipay", + "pay", + "payment", + "stripe" + ], + "support": { + "issues": "https://github.com/thephpleague/omnipay-stripe/issues", + "source": "https://github.com/thephpleague/omnipay-stripe/tree/v3.2.0" + }, + "time": "2022-03-22T10:58:41+00:00" + }, + { + "name": "opcodesio/log-viewer", + "version": "dev-lycheeOrg", + "source": { + "type": "git", + "url": "https://github.com/LycheeOrg/log-viewer.git", + "reference": "a6a0f8a19bcc5b25e82ce79bfb32e877c191c0a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/LycheeOrg/log-viewer/zipball/a6a0f8a19bcc5b25e82ce79bfb32e877c191c0a1", + "reference": "a6a0f8a19bcc5b25e82ce79bfb32e877c191c0a1", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "opcodesio/mail-parser": "^0.2.3", + "php": "^8.0" + }, + "conflict": { + "arcanedev/log-viewer": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.2", + "itsgoingd/clockwork": "^5.1", + "laravel/pint": "^1.0", + "nunomaduro/collision": "^7.0|^8.0", + "orchestra/testbench": "^7.6|^8.0|^9.5|^10.0|^11.0", + "pestphp/pest": "^2.0|^3.7|^4.4", + "pestphp/pest-plugin-laravel": "^2.0|^3.1|^4.1", + "spatie/test-time": "^1.3" + }, + "suggest": { + "guzzlehttp/guzzle": "Required for multi-host support. ^7.2" + }, + "default-branch": true, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Opcodes\\LogViewer\\LogViewerServiceProvider" + ], + "aliases": { + "LogViewer": "Opcodes\\LogViewer\\Facades\\LogViewer" + } + } + }, + "autoload": { + "psr-4": { + "Opcodes\\LogViewer\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Opcodes\\LogViewer\\Tests\\": "tests" + } + }, + "scripts": { + "analyse": [ + "echo \"Static analysis not configured yet.\" && exit 0" + ], + "test": [ + "vendor/bin/pest --order-by random" + ], + "test-coverage": [ + "echo \"Test coverage not configured yet.\" && exit 0" + ], + "format": [ + "vendor/bin/pint" + ] + }, + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arunas Skirius", + "email": "arukomp@gmail.com", + "role": "Developer" + } + ], + "description": "Fast and easy-to-use log viewer for your Laravel application", + "homepage": "https://github.com/opcodesio/log-viewer", + "keywords": [ + "arukompas", + "better-log-viewer", + "laravel", + "log viewer", + "logs", + "opcodesio" + ], + "support": { + "source": "https://github.com/LycheeOrg/log-viewer/tree/lycheeOrg" + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/arukompas" + }, + { + "type": "custom", + "url": "https://www.buymeacoffee.com/arunas" + } + ], + "time": "2026-03-22T20:17:13+00:00" + }, + { + "name": "opcodesio/mail-parser", + "version": "v0.2.4", + "source": { + "type": "git", + "url": "https://github.com/opcodesio/mail-parser.git", + "reference": "7151e4183288e46d911a76803c5545a1c8822226" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opcodesio/mail-parser/zipball/7151e4183288e46d911a76803c5545a1c8822226", + "reference": "7151e4183288e46d911a76803c5545a1c8822226", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "pestphp/pest": "^2.16", + "symfony/var-dumper": "^6.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Opcodes\\MailParser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arunas Skirius", + "email": "arukomp@gmail.com", + "role": "Developer" + } + ], + "description": "Parse emails without the mailparse extension", + "keywords": [ + "arukompas", + "email", + "email parser", + "mail", + "opcodesio", + "php" + ], + "support": { + "issues": "https://github.com/opcodesio/mail-parser/issues", + "source": "https://github.com/opcodesio/mail-parser/tree/v0.2.4" + }, + "time": "2026-06-11T08:50:37+00:00" + }, + { + "name": "open-telemetry/api", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/api.git", + "reference": "7c029c4a6fd457094a20569bf98f93d95e9a7559" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/7c029c4a6fd457094a20569bf98f93d95e9a7559", + "reference": "7c029c4a6fd457094a20569bf98f93d95e9a7559", + "shasum": "" + }, + "require": { + "open-telemetry/context": "^1.4", + "php": "^8.1", + "psr/log": "^1.1|^2.0|^3.0", + "symfony/polyfill-php82": "^1.26" + }, + "conflict": { + "open-telemetry/sdk": "<=1.11" + }, + "type": "library", + "extra": { + "spi": { + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" + ] + }, + "branch-alias": { + "dev-main": "1.8.x-dev" + } + }, + "autoload": { + "files": [ + "Trace/functions.php" + ], + "psr-4": { + "OpenTelemetry\\API\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "API for OpenTelemetry PHP.", + "keywords": [ + "Metrics", + "api", + "apm", + "logging", + "opentelemetry", + "otel", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/languages/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2026-07-06T12:28:04+00:00" + }, + { + "name": "open-telemetry/context", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/context.git", + "reference": "3c414b246e0dabb7d6145404e6a5e4536ca18d07" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/3c414b246e0dabb7d6145404e6a5e4536ca18d07", + "reference": "3c414b246e0dabb7d6145404e6a5e4536ca18d07", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/polyfill-php82": "^1.26" + }, + "suggest": { + "ext-ffi": "To allow context switching in Fibers" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "files": [ + "fiber/initialize_fiber_handler.php" + ], + "psr-4": { + "OpenTelemetry\\Context\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "Context implementation for OpenTelemetry PHP.", + "keywords": [ + "Context", + "opentelemetry", + "otel" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/languages/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2025-10-19T06:44:33+00:00" + }, + { + "name": "open-telemetry/exporter-otlp", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/exporter-otlp.git", + "reference": "283a0d66522f2adc6d8d7debfd7686be91c282be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/283a0d66522f2adc6d8d7debfd7686be91c282be", + "reference": "283a0d66522f2adc6d8d7debfd7686be91c282be", + "shasum": "" + }, + "require": { + "open-telemetry/api": "^1.0", + "open-telemetry/gen-otlp-protobuf": "^1.1", + "open-telemetry/sdk": "^1.0", + "php": "^8.1", + "php-http/discovery": "^1.14" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "files": [ + "_register.php" + ], + "psr-4": { + "OpenTelemetry\\Contrib\\Otlp\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "OTLP exporter for OpenTelemetry.", + "keywords": [ + "Metrics", + "exporter", + "gRPC", + "http", + "opentelemetry", + "otel", + "otlp", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/languages/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2026-02-05T09:44:52+00:00" + }, + { + "name": "open-telemetry/gen-otlp-protobuf", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git", + "reference": "66f04d0e448ad333033bfc7baae1aa56330be088" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/66f04d0e448ad333033bfc7baae1aa56330be088", + "reference": "66f04d0e448ad333033bfc7baae1aa56330be088", + "shasum": "" + }, + "require": { + "google/protobuf": "^3.22 || ^4.0 || ^5.0", + "php": "^8.0" + }, + "suggest": { + "ext-protobuf": "For better performance, when dealing with the protobuf format" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opentelemetry\\Proto\\": "Opentelemetry/Proto/", + "GPBMetadata\\Opentelemetry\\": "GPBMetadata/Opentelemetry/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "PHP protobuf files for communication with OpenTelemetry OTLP collectors/servers.", + "keywords": [ + "Metrics", + "apm", + "gRPC", + "logging", + "opentelemetry", + "otel", + "otlp", + "protobuf", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/languages/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2026-06-17T12:06:32+00:00" + }, + { + "name": "open-telemetry/opentelemetry-auto-guzzle", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/contrib-auto-guzzle.git", + "reference": "1553d9ab2e0a94c8af5d8c83946cb8dfa71835ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/contrib-auto-guzzle/zipball/1553d9ab2e0a94c8af5d8c83946cb8dfa71835ec", + "reference": "1553d9ab2e0a94c8af5d8c83946cb8dfa71835ec", + "shasum": "" + }, + "require": { + "ext-opentelemetry": "*", + "guzzlehttp/guzzle": "^7", + "open-telemetry/api": "^1.0", + "open-telemetry/sem-conv": "^1.32", + "php": "^8.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "guzzlehttp/promises": "^2", + "nyholm/psr7": "*", + "open-telemetry/sdk": "^1.0", + "phan/phan": "^5.0", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-mockery": "^1.1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5", + "psalm/plugin-phpunit": "^0.19.2", + "vimeo/psalm": "6.4.0" + }, + "type": "library", + "autoload": { + "files": [ + "_register.php" + ], + "psr-4": { + "OpenTelemetry\\Contrib\\Instrumentation\\Guzzle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "OpenTelemetry auto-instrumentation for Guzzle.", + "homepage": "https://opentelemetry.io/docs/languages/php/", + "keywords": [ + "Guzzle", + "instrumentation", + "open-telemetry", + "opentelemetry", + "otel", + "tracing" + ], + "support": { + "source": "https://github.com/opentelemetry-php/contrib-auto-guzzle/tree/1.4.0" + }, + "time": "2026-07-09T20:17:32+00:00" + }, + { + "name": "open-telemetry/opentelemetry-auto-laravel", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/contrib-auto-laravel.git", + "reference": "f0a750eeb0a69012d81c84010a4c7a7b61926e00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/contrib-auto-laravel/zipball/f0a750eeb0a69012d81c84010a4c7a7b61926e00", + "reference": "f0a750eeb0a69012d81c84010a4c7a7b61926e00", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-opentelemetry": "*", + "laravel/framework": "^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0", + "open-telemetry/api": "^1.8", + "open-telemetry/sem-conv": "^1.38", + "php": "^8.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.50", + "guzzlehttp/guzzle": "*", + "nunomaduro/collision": "*", + "open-telemetry/sdk": "^1.8", + "orchestra/testbench": ">=7.41.3", + "phan/phan": "^5.0", + "php-http/mock-client": "*", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5 || ^10 || ^11 || ^12 || ^13", + "psalm/plugin-phpunit": "^0.19.2", + "spatie/laravel-ignition": "*", + "vimeo/psalm": "6.4.0" + }, + "suggest": { + "open-telemetry/opentelemetry-propagation-server-timing": "Automatically propagate the context to the client through server-timing headers.", + "open-telemetry/opentelemetry-propagation-traceresponse": "Automatically propagate the context to the client through trace-response headers." + }, + "type": "library", + "autoload": { + "files": [ + "_register.php" + ], + "psr-4": { + "OpenTelemetry\\Contrib\\Instrumentation\\Laravel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "OpenTelemetry auto-instrumentation for Laravel", + "homepage": "https://opentelemetry.io/docs/languages/php/", + "keywords": [ + "instrumentation", + "laravel", + "open-telemetry", + "opentelemetry", + "otel", + "tracing" + ], + "support": { + "source": "https://github.com/opentelemetry-php/contrib-auto-laravel/tree/1.8.0" + }, + "time": "2026-07-09T20:34:43+00:00" + }, + { + "name": "open-telemetry/opentelemetry-auto-pdo", + "version": "0.5.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/contrib-auto-pdo.git", + "reference": "ef6f83d48f02669dbe9a812130bdb3fbd46651e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/contrib-auto-pdo/zipball/ef6f83d48f02669dbe9a812130bdb3fbd46651e6", + "reference": "ef6f83d48f02669dbe9a812130bdb3fbd46651e6", + "shasum": "" + }, + "require": { + "ext-opentelemetry": "*", + "ext-pdo": "*", + "open-telemetry/api": "^1.0", + "open-telemetry/sem-conv": "^1.36", + "php": "^8.2", + "symfony/polyfill-mbstring": "^1.31" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "open-telemetry/sdk": "^1.0", + "open-telemetry/test-utils": "^0.2.0", + "phan/phan": "^5.0", + "php-http/mock-client": "*", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5", + "psalm/plugin-phpunit": "^0.19.2", + "vimeo/psalm": "6.4.0" + }, + "suggest": { + "ext-mbstring": "For better performance than symfony/polyfill-mbstring" + }, + "type": "library", + "autoload": { + "files": [ + "_register.php" + ], + "psr-4": { + "OpenTelemetry\\Contrib\\Instrumentation\\PDO\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" - ], - "authors": [ - { - "name": "Adrian Macneil", - "email": "adrian@adrianmacneil.com" - }, - { - "name": "Omnipay Contributors", - "homepage": "https://github.com/thephpleague/omnipay-stripe/contributors" - } + "Apache-2.0" ], - "description": "Stripe driver for the Omnipay payment processing library", - "homepage": "https://github.com/thephpleague/omnipay-stripe", + "description": "OpenTelemetry auto-instrumentation for PDO", + "homepage": "https://opentelemetry.io/docs/languages/php/", "keywords": [ - "gateway", - "merchant", - "omnipay", - "pay", - "payment", - "stripe" + "instrumentation", + "open-telemetry", + "opentelemetry", + "otel", + "pdo", + "tracing" ], "support": { - "issues": "https://github.com/thephpleague/omnipay-stripe/issues", - "source": "https://github.com/thephpleague/omnipay-stripe/tree/v3.2.0" + "source": "https://github.com/opentelemetry-php/contrib-auto-pdo/tree/0.5.0" }, - "time": "2022-03-22T10:58:41+00:00" + "time": "2026-06-08T12:54:17+00:00" }, { - "name": "opcodesio/log-viewer", - "version": "dev-lycheeOrg", + "name": "open-telemetry/sdk", + "version": "1.15.0", "source": { "type": "git", - "url": "https://github.com/LycheeOrg/log-viewer.git", - "reference": "a6a0f8a19bcc5b25e82ce79bfb32e877c191c0a1" + "url": "https://github.com/opentelemetry-php/sdk.git", + "reference": "77e1aa73850154abb86937d52a70883edc3b4547" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/LycheeOrg/log-viewer/zipball/a6a0f8a19bcc5b25e82ce79bfb32e877c191c0a1", - "reference": "a6a0f8a19bcc5b25e82ce79bfb32e877c191c0a1", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/77e1aa73850154abb86937d52a70883edc3b4547", + "reference": "77e1aa73850154abb86937d52a70883edc3b4547", "shasum": "" }, "require": { - "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", - "opcodesio/mail-parser": "^0.2.3", - "php": "^8.0" - }, - "conflict": { - "arcanedev/log-viewer": "^8.0" - }, - "require-dev": { - "guzzlehttp/guzzle": "^7.2", - "itsgoingd/clockwork": "^5.1", - "laravel/pint": "^1.0", - "nunomaduro/collision": "^7.0|^8.0", - "orchestra/testbench": "^7.6|^8.0|^9.5|^10.0|^11.0", - "pestphp/pest": "^2.0|^3.7|^4.4", - "pestphp/pest-plugin-laravel": "^2.0|^3.1|^4.1", - "spatie/test-time": "^1.3" + "ext-json": "*", + "nyholm/psr7-server": "^1.1", + "open-telemetry/api": "^1.8", + "open-telemetry/context": "^1.4", + "open-telemetry/sem-conv": "^1.38.0", + "php": "^8.1", + "php-http/discovery": "^1.14", + "psr/http-client": "^1.0", + "psr/http-client-implementation": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/http-message": "^1.0.1|^2.0", + "psr/log": "^1.1|^2.0|^3.0", + "ramsey/uuid": "^3.0 || ^4.0", + "symfony/polyfill-mbstring": "^1.23", + "symfony/polyfill-php82": "^1.26", + "tbachert/spi": "^1.0.5" }, "suggest": { - "guzzlehttp/guzzle": "Required for multi-host support. ^7.2" + "ext-gmp": "To support unlimited number of synchronous metric readers", + "ext-mbstring": "To increase performance of string operations", + "open-telemetry/sdk-configuration": "File-based OpenTelemetry SDK configuration" }, - "default-branch": true, "type": "library", "extra": { - "laravel": { - "providers": [ - "Opcodes\\LogViewer\\LogViewerServiceProvider" + "spi": { + "OpenTelemetry\\API\\Configuration\\ConfigEnv\\EnvComponentLoader": [ + "OpenTelemetry\\API\\Instrumentation\\Configuration\\General\\ConfigEnv\\EnvComponentLoaderHttpConfig", + "OpenTelemetry\\API\\Instrumentation\\Configuration\\General\\ConfigEnv\\EnvComponentLoaderPeerConfig", + "OpenTelemetry\\SDK\\ConfigEnv\\Trace\\SpanSuppressionStrategySemConv", + "OpenTelemetry\\SDK\\ConfigEnv\\Trace\\SpanSuppressionStrategySpanKind", + "OpenTelemetry\\SDK\\ConfigEnv\\Distribution\\DistributionConfigurationSdk" ], - "aliases": { - "LogViewer": "Opcodes\\LogViewer\\Facades\\LogViewer" - } + "OpenTelemetry\\SDK\\Common\\Configuration\\Resolver\\ResolverInterface": [ + "OpenTelemetry\\SDK\\Common\\Configuration\\Resolver\\SdkConfigurationResolver" + ], + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" + ] + }, + "branch-alias": { + "dev-main": "1.14.x-dev" } }, "autoload": { + "files": [ + "Common/Util/functions.php", + "Logs/Exporter/_register.php", + "Metrics/MetricExporter/_register.php", + "Propagation/_register.php", + "Trace/SpanExporter/_register.php", + "Common/Dev/Compatibility/_load.php", + "_autoload.php" + ], "psr-4": { - "Opcodes\\LogViewer\\": "src" - } - }, - "autoload-dev": { - "psr-4": { - "Opcodes\\LogViewer\\Tests\\": "tests" + "OpenTelemetry\\SDK\\": "." } }, - "scripts": { - "analyse": [ - "echo \"Static analysis not configured yet.\" && exit 0" - ], - "test": [ - "vendor/bin/pest --order-by random" - ], - "test-coverage": [ - "echo \"Test coverage not configured yet.\" && exit 0" - ], - "format": [ - "vendor/bin/pint" - ] - }, + "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "Apache-2.0" ], "authors": [ { - "name": "Arunas Skirius", - "email": "arukomp@gmail.com", - "role": "Developer" + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" } ], - "description": "Fast and easy-to-use log viewer for your Laravel application", - "homepage": "https://github.com/opcodesio/log-viewer", + "description": "SDK for OpenTelemetry PHP.", "keywords": [ - "arukompas", - "better-log-viewer", - "laravel", - "log viewer", - "logs", - "opcodesio" + "Metrics", + "apm", + "logging", + "opentelemetry", + "otel", + "sdk", + "tracing" ], "support": { - "source": "https://github.com/LycheeOrg/log-viewer/tree/lycheeOrg" + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/languages/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "funding": [ - { - "type": "github", - "url": "https://github.com/arukompas" - }, - { - "type": "custom", - "url": "https://www.buymeacoffee.com/arunas" - } - ], - "time": "2026-03-22T20:17:13+00:00" + "time": "2026-07-14T13:09:54+00:00" }, { - "name": "opcodesio/mail-parser", - "version": "v0.2.4", + "name": "open-telemetry/sem-conv", + "version": "1.38.0", "source": { "type": "git", - "url": "https://github.com/opcodesio/mail-parser.git", - "reference": "7151e4183288e46d911a76803c5545a1c8822226" + "url": "https://github.com/opentelemetry-php/sem-conv.git", + "reference": "e613bc640a407def4991b8a936a9b27edd9a3240" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opcodesio/mail-parser/zipball/7151e4183288e46d911a76803c5545a1c8822226", - "reference": "7151e4183288e46d911a76803c5545a1c8822226", + "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/e613bc640a407def4991b8a936a9b27edd9a3240", + "reference": "e613bc640a407def4991b8a936a9b27edd9a3240", "shasum": "" }, "require": { "php": "^8.0" }, - "require-dev": { - "pestphp/pest": "^2.16", - "symfony/var-dumper": "^6.3" - }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, "autoload": { "psr-4": { - "Opcodes\\MailParser\\": "src/" + "OpenTelemetry\\SemConv\\": "." } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "Apache-2.0" ], "authors": [ { - "name": "Arunas Skirius", - "email": "arukomp@gmail.com", - "role": "Developer" + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" } ], - "description": "Parse emails without the mailparse extension", + "description": "Semantic conventions for OpenTelemetry PHP.", "keywords": [ - "arukompas", - "email", - "email parser", - "mail", - "opcodesio", - "php" + "Metrics", + "apm", + "logging", + "opentelemetry", + "otel", + "semantic conventions", + "semconv", + "tracing" ], "support": { - "issues": "https://github.com/opcodesio/mail-parser/issues", - "source": "https://github.com/opcodesio/mail-parser/tree/v0.2.4" + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/languages/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2026-06-11T08:50:37+00:00" + "time": "2026-01-21T04:14:03+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -10916,6 +11635,86 @@ ], "time": "2026-04-10T16:19:22+00:00" }, + { + "name": "symfony/polyfill-php82", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php82.git", + "reference": "002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b", + "reference": "002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php82\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php82/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T12:45:58+00:00" + }, { "name": "symfony/polyfill-php83", "version": "v1.41.0", @@ -12076,6 +12875,58 @@ ], "time": "2026-07-30T12:40:56+00:00" }, + { + "name": "tbachert/spi", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/Nevay/spi.git", + "reference": "e7078767866d0a9e0f91d3f9d42a832df5e39002" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nevay/spi/zipball/e7078767866d0a9e0f91d3f9d42a832df5e39002", + "reference": "e7078767866d0a9e0f91d3f9d42a832df5e39002", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "composer/semver": "^1.0 || ^2.0 || ^3.0", + "php": "^8.1" + }, + "require-dev": { + "composer/composer": "^2.0", + "infection/infection": "^0.27.9", + "phpunit/phpunit": "^10.5", + "psalm/phar": "^5.18" + }, + "type": "composer-plugin", + "extra": { + "class": "Nevay\\SPI\\Composer\\Plugin", + "branch-alias": { + "dev-main": "1.0.x-dev" + }, + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Nevay\\SPI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "Service provider loading facility", + "keywords": [ + "service provider" + ], + "support": { + "issues": "https://github.com/Nevay/spi/issues", + "source": "https://github.com/Nevay/spi/tree/v1.0.5" + }, + "time": "2025-06-29T15:42:06+00:00" + }, { "name": "thecodingmachine/safe", "version": "v3.4.0",