diff options
-rw-r--r-- | includes/api/ApiUpload.php | 148 | ||||
-rw-r--r-- | includes/upload/UploadBase.php | 34 | ||||
-rw-r--r-- | includes/upload/UploadFromChunks.php | 56 |
3 files changed, 229 insertions, 9 deletions
diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php index 06d517670e67..53fb33a424e7 100644 --- a/includes/api/ApiUpload.php +++ b/includes/api/ApiUpload.php @@ -21,6 +21,7 @@ */ use MediaWiki\Config\Config; +use MediaWiki\Logger\LoggerFactory; use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\Status\Status; @@ -28,6 +29,7 @@ use MediaWiki\Title\Title; use MediaWiki\User\Options\UserOptionsLookup; use MediaWiki\User\User; use MediaWiki\Watchlist\WatchlistManager; +use Psr\Log\LoggerInterface; use Wikimedia\ParamValidator\ParamValidator; use Wikimedia\ParamValidator\TypeDef\IntegerDef; @@ -45,6 +47,8 @@ class ApiUpload extends ApiBase { private JobQueueGroup $jobQueueGroup; + private LoggerInterface $log; + /** * @param ApiMain $mainModule * @param string $moduleName @@ -68,6 +72,7 @@ class ApiUpload extends ApiBase { $this->getConfig()->get( MainConfigNames::WatchlistExpiryMaxDuration ); $this->watchlistManager = $watchlistManager; $this->userOptionsLookup = $userOptionsLookup; + $this->log = LoggerFactory::getInstance( 'upload' ); } public function execute() { @@ -111,6 +116,13 @@ class ApiUpload extends ApiBase { /** @var Status $status */ $status = $this->mUpload->fetchFile(); if ( !$status->isGood() ) { + $this->log->info( "Unable to fetch file {filename} for {user} because {status}", + [ + 'user' => $this->getUser()->getName(), + 'status' => (string)$status, + 'filename' => $this->mParams['filename'] ?? '-', + ] + ); $this->dieStatus( $status ); } @@ -313,6 +325,14 @@ class ApiUpload extends ApiBase { } if ( $this->mParams['offset'] == 0 ) { + $this->log->debug( "Started first chunk of chunked upload of {filename} for {user}", + [ + 'user' => $this->getUser()->getName(), + 'filename' => $this->mParams['filename'] ?? '-', + 'filesize' => $this->mParams['filesize'], + 'chunkSize' => $chunkSize + ] + ); $filekey = $this->performStash( 'critical' ); } else { $filekey = $this->mParams['filekey']; @@ -321,6 +341,15 @@ class ApiUpload extends ApiBase { $progress = UploadBase::getSessionStatus( $this->getUser(), $filekey ); if ( !$progress ) { // Probably can't get here, but check anyway just in case + $this->log->info( "Stash failed due to no session for {user}", + [ + 'user' => $this->getUser()->getName(), + 'filename' => $this->mParams['filename'] ?? '-', + 'filekey' => $this->mParams['filekey'] ?? '-', + 'filesize' => $this->mParams['filesize'], + 'chunkSize' => $chunkSize + ] + ); $this->dieWithError( 'apierror-stashfailed-nosession', 'stashfailed' ); } elseif ( $progress['result'] !== 'Continue' || $progress['stage'] !== 'uploading' ) { $this->dieWithError( 'apierror-stashfailed-complete', 'stashfailed' ); @@ -332,8 +361,29 @@ class ApiUpload extends ApiBase { $extradata = [ 'offset' => $this->mUpload->getOffset(), ]; - + $this->log->info( "Chunked upload stash failure {status} for {user}", + [ + 'status' => (string)$status, + 'user' => $this->getUser()->getName(), + 'filename' => $this->mParams['filename'] ?? '-', + 'filekey' => $this->mParams['filekey'] ?? '-', + 'filesize' => $this->mParams['filesize'], + 'chunkSize' => $chunkSize, + 'offset' => $this->mUpload->getOffset() + ] + ); $this->dieStatusWithCode( $status, 'stashfailed', $extradata ); + } else { + $this->log->debug( "Got chunk for {filename} with offset {offset} for {user}", + [ + 'user' => $this->getUser()->getName(), + 'filename' => $this->mParams['filename'] ?? '-', + 'filekey' => $this->mParams['filekey'] ?? '-', + 'filesize' => $this->mParams['filesize'], + 'chunkSize' => $chunkSize, + 'offset' => $this->mUpload->getOffset() + ] + ); } } @@ -354,9 +404,28 @@ class ApiUpload extends ApiBase { 'session' => $this->getContext()->exportSession() ] ) ); + $this->log->info( "Received final chunk of {filename} for {user}, queuing assemble job", + [ + 'user' => $this->getUser()->getName(), + 'filename' => $this->mParams['filename'] ?? '-', + 'filekey' => $this->mParams['filekey'] ?? '-', + 'filesize' => $this->mParams['filesize'], + 'chunkSize' => $chunkSize, + ] + ); $result['result'] = 'Poll'; $result['stage'] = 'queued'; } else { + $this->log->info( "Received final chunk of {filename} for {user}, assembling immediately", + [ + 'user' => $this->getUser()->getName(), + 'filename' => $this->mParams['filename'] ?? '-', + 'filekey' => $this->mParams['filekey'] ?? '-', + 'filesize' => $this->mParams['filesize'], + 'chunkSize' => $chunkSize, + ] + ); + $status = $this->mUpload->concatenateChunks(); if ( !$status->isGood() ) { UploadBase::setSessionStatus( @@ -364,6 +433,15 @@ class ApiUpload extends ApiBase { $filekey, [ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ] ); + $this->log->info( "Non jobqueue assembly of {filename} failed because {status}", + [ + 'user' => $this->getUser()->getName(), + 'filename' => $this->mParams['filename'] ?? '-', + 'filekey' => $this->mParams['filekey'] ?? '-', + 'filesize' => $this->mParams['filesize'], + 'chunkSize' => $chunkSize, + ] + ); $this->dieStatusWithCode( $status, 'stashfailed' ); } @@ -424,7 +502,14 @@ class ApiUpload extends ApiBase { } } catch ( Exception $e ) { $debugMessage = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage(); - wfDebug( __METHOD__ . ' ' . $debugMessage ); + $this->log->info( $debugMessage, + [ + 'user' => $this->getUser()->getName(), + 'filename' => $this->mParams['filename'] ?? '-', + 'filekey' => $this->mParams['filekey'] ?? '-' + ] + ); + $status = Status::newFatal( $this->getErrorFormatter()->getMessageFromException( $e, [ 'wrap' => new ApiMessage( 'apierror-stashexception', 'stashfailed' ) ] ) ); @@ -443,9 +528,23 @@ class ApiUpload extends ApiBase { // Statuses for it. Just extract the exception details and parse them ourselves. [ $exceptionType, $message ] = $status->getMessage()->getParams(); $debugMessage = 'Stashing temporary file failed: ' . $exceptionType . ' ' . $message; - wfDebug( __METHOD__ . ' ' . $debugMessage ); + $this->log->info( $debugMessage, + [ + 'user' => $this->getUser()->getName(), + 'filename' => $this->mParams['filename'] ?? '-', + 'filekey' => $this->mParams['filekey'] ?? '-' + ] + ); } + $this->log->info( "Stash upload failure {status}", + [ + 'status' => (string)$status, + 'user' => $this->getUser()->getName(), + 'filename' => $this->mParams['filename'] ?? '-', + 'filekey' => $this->mParams['filekey'] ?? '-' + ] + ); // Bad status if ( $failureMode !== 'optional' ) { $this->dieStatus( $status ); @@ -524,6 +623,13 @@ class ApiUpload extends ApiBase { if ( $this->mParams['filekey'] && $this->mParams['checkstatus'] ) { $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] ); if ( !$progress ) { + $this->log->info( "Cannot check upload status due to missing upload session for {user}", + [ + 'user' => $this->getUser()->getName(), + 'filename' => $this->mParams['filename'] ?? '-', + 'filekey' => $this->mParams['filekey'] ?? '-' + ] + ); $this->dieWithError( 'apierror-upload-missingresult', 'missingresult' ); } elseif ( !$progress['status']->isGood() ) { $this->dieStatusWithCode( $progress['status'], 'stashfailed' ); @@ -685,8 +791,19 @@ class ApiUpload extends ApiBase { wfDebug( __METHOD__ . " about to verify" ); $verification = $this->mUpload->verifyUpload(); + if ( $verification['status'] === UploadBase::OK ) { return; + } else { + $this->log->info( "File verification of {filename} failed for {user} because {result}", + [ + 'user' => $this->getUser()->getName(), + 'resultCode' => $verification['status'], + 'result' => $this->mUpload->getVerificationErrorCode( $verification['status'] ), + 'filename' => $this->mParams['filename'] ?? '-', + 'details' => $verification['details'] ?? '' + ] + ); } } @@ -847,6 +964,15 @@ class ApiUpload extends ApiBase { * @return StatusValue */ protected function handleStashException( $e ) { + $this->log->info( "Upload stashing of {filename} failed for {user} because {error}", + [ + 'user' => $this->getUser()->getName(), + 'status' => get_class( $e ), + 'filename' => $this->mParams['filename'] ?? '-', + 'filekey' => $this->mParams['filekey'] ?? '-' + ] + ); + switch ( get_class( $e ) ) { case UploadStashFileNotFoundException::class: $wrap = 'apierror-stashedfilenotfound'; @@ -948,6 +1074,14 @@ class ApiUpload extends ApiBase { 'session' => $this->getContext()->exportSession() ] ) ); + $this->log->info( "Sending publish job of {filename} for {user}", + [ + 'user' => $this->getUser()->getName(), + 'filename' => $this->mParams['filename'] ?? '-', + 'filekey' => $this->mParams['filekey'] ?? '-' + ] + + ); $result['result'] = 'Poll'; $result['stage'] = 'queued'; } else { @@ -962,6 +1096,14 @@ class ApiUpload extends ApiBase { ); if ( !$status->isGood() ) { + $this->log->info( "Non-async API upload publish failed for {user} because {status}", + [ + 'user' => $this->getUser()->getName(), + 'filename' => $this->mParams['filename'] ?? '-', + 'filekey' => $this->mParams['filekey'] ?? '-', + 'status' => (string)$status + ] + ); $this->dieRecoverableError( $status->getErrors() ); } $result['result'] = 'Success'; diff --git a/includes/upload/UploadBase.php b/includes/upload/UploadBase.php index ab8df0602a96..392a60391c31 100644 --- a/includes/upload/UploadBase.php +++ b/includes/upload/UploadBase.php @@ -24,6 +24,7 @@ use MediaWiki\Context\RequestContext; use MediaWiki\HookContainer\HookRunner; use MediaWiki\HookContainer\ProtectedHookAccessorTrait; +use MediaWiki\Logger\LoggerFactory; use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\Parser\Sanitizer; @@ -2250,6 +2251,39 @@ abstract class UploadBase { public static function setSessionStatus( UserIdentity $user, $statusKey, $value ) { $store = self::getUploadSessionStore(); $key = self::getUploadSessionKey( $store, $user, $statusKey ); + $logger = LoggerFactory::getInstance( 'upload' ); + + if ( is_array( $value ) && ( $value['result'] ?? '' ) === 'Failure' ) { + $logger->info( 'Upload session {key} for {user} set to failure {status} at {stage}', + [ + 'result' => $value['result'] ?? '', + 'stage' => $value['stage'] ?? 'unknown', + 'user' => $user->getName(), + 'status' => (string)( $value['status'] ?? '-' ), + 'filekey' => $value['filekey'] ?? '', + 'key' => $statusKey + ] + ); + } elseif ( is_array( $value ) ) { + $logger->debug( 'Upload session {key} for {user} changed {status} at {stage}', + [ + 'result' => $value['result'] ?? '', + 'stage' => $value['stage'] ?? 'unknown', + 'user' => $user->getName(), + 'status' => (string)( $value['status'] ?? '-' ), + 'filekey' => $value['filekey'] ?? '', + 'key' => $statusKey + ] + ); + } else { + $logger->debug( "Upload session {key} deleted for {user}", + [ + 'value' => $value, + 'key' => $statusKey, + 'user' => $user->getName() + ] + ); + } if ( $value === false ) { $store->delete( $key ); diff --git a/includes/upload/UploadFromChunks.php b/includes/upload/UploadFromChunks.php index 452951e66269..2906a565435c 100644 --- a/includes/upload/UploadFromChunks.php +++ b/includes/upload/UploadFromChunks.php @@ -5,6 +5,7 @@ use MediaWiki\MediaWikiServices; use MediaWiki\Request\WebRequestUpload; use MediaWiki\Status\Status; use MediaWiki\User\User; +use Psr\Log\LoggerInterface; /** * Backend for uploading files from chunks. @@ -47,6 +48,8 @@ class UploadFromChunks extends UploadFromFile { protected $mFileKey; protected $mVirtualTempPath; + private LoggerInterface $logger; + /** @noinspection PhpMissingParentConstructorInspection */ /** @@ -71,6 +74,8 @@ class UploadFromChunks extends UploadFromFile { wfDebug( __METHOD__ . " creating new UploadFromChunks instance for " . $user->getId() ); $this->stash = new UploadStash( $this->repo, $this->user ); } + + $this->logger = LoggerFactory::getInstance( 'upload' ); } /** @@ -139,8 +144,14 @@ class UploadFromChunks extends UploadFromFile { */ public function concatenateChunks() { $chunkIndex = $this->getChunkIndex(); - wfDebug( __METHOD__ . " concatenate {$this->mChunkIndex} chunks:" . - $this->getOffset() . ' inx:' . $chunkIndex ); + $this->logger->debug( + __METHOD__ . ' concatenate {totalChunks} chunks: {offset} inx: {curIndex}', + [ + 'offset' => $this->getOffset(), + 'totalChunks' => $this->mChunkIndex, + 'curIndex' => $chunkIndex, + ] + ); // Concatenate all the chunks to mVirtualTempPath $fileList = []; @@ -158,6 +169,8 @@ class UploadFromChunks extends UploadFromFile { if ( $tmpFile ) { // keep alive with $this $tmpPath = $tmpFile->bind( $this )->getPath(); + } else { + $this->logger->warning( "Error getting tmp file" ); } // Concatenate the chunks at the temp file @@ -182,7 +195,7 @@ class UploadFromChunks extends UploadFromFile { $ret = $this->verifyUpload(); if ( $ret['status'] !== UploadBase::OK ) { - wfDebugLog( 'fileconcatenate', "Verification failed for chunked upload" ); + $this->logger->info( "Verification failed for chunked upload", [ 'user' => $this->user->getName() ] ); $status->fatal( $this->getVerificationErrorCode( $ret['status'] ) ); return $status; @@ -196,11 +209,26 @@ class UploadFromChunks extends UploadFromFile { $error = $this->runUploadStashFileHook( $this->user ); if ( $error ) { $status->fatal( ...$error ); + $this->logger->info( "Aborting stash upload due to hook - {status}", + [ + 'status' => (string)$status, + 'user' => $this->user->getName(), + 'filekey' => $this->mFileKey + ] + ); return $status; } try { $this->mStashFile = parent::doStashFile( $this->user ); } catch ( UploadStashException $e ) { + $this->logger->warning( "Could not stash file for {user} because {error} {msg}", + [ + 'user' => $this->user->getName(), + 'error' => get_class( $e ), + 'msg' => $e->getMessage(), + 'filekey' => $this->mFileKey + ] + ); $status->fatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() ); return $status; } @@ -253,6 +281,15 @@ class UploadFromChunks extends UploadFromFile { $this->verifyChunk(); $this->mTempPath = $oldTemp; } catch ( UploadChunkVerificationException $e ) { + $this->logger->info( "Error verifying upload chunk {msg}", + [ + 'user' => $this->user->getName(), + 'msg' => $e->getMessage(), + 'chunkIndex' => $this->mChunkIndex, + 'filekey' => $this->mFileKey + ] + ); + return Status::newFatal( $e->msg ); } $status = $this->outputChunk( $chunkPath ); @@ -274,8 +311,14 @@ class UploadFromChunks extends UploadFromFile { * Update the chunk db table with the current status: */ private function updateChunkStatus() { - wfDebug( __METHOD__ . " update chunk status for {$this->mFileKey} offset:" . - $this->getOffset() . ' inx:' . $this->getChunkIndex() ); + $this->logger->info( "update chunk status for {filekey} offset: {offset} inx: {inx}", + [ + 'offset' => $this->getOffset(), + 'inx' => $this->getChunkIndex(), + 'filekey' => $this->mFileKey, + 'user' => $this->user->getName() + ] + ); $dbw = $this->repo->getPrimaryDB(); $dbw->newUpdateQueryBuilder() @@ -395,7 +438,7 @@ class UploadFromChunks extends UploadFromFile { * @return array */ private function logFileBackendStatus( Status $status, string $logMessage, array $context = [] ): array { - $logger = LoggerFactory::getInstance( 'upload' ); + $logger = $this->logger; $errorToThrow = null; $warningToThrow = null; @@ -407,6 +450,7 @@ class UploadFromChunks extends UploadFromFile { // The message arguments often contains the name of the failing datacenter or file names // and should not show up in aggregated stats, add to context $context['details'] = implode( '; ', $errorItem['params'] ); + $context['user'] = $this->user->getName(); if ( $errorItem['type'] === 'error' ) { // Use the first error of the list for the exception text |