Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions lib/Http/Client/Middleware/CountingStream.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,7 @@ private function writeLog(bool $complete): void {

[$jsonFile, $plainFile] = LogPathHelper::getPathsFromMeta($this->meta, $this->logBaseDir, $this->reqId);

if (in_array($this->logFormat, ['json', 'both'], true)) {
@file_put_contents($jsonFile, json_encode($merged, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL, FILE_APPEND | LOCK_EX);
}
if (in_array($this->logFormat, ['plain', 'both'], true)) {
@file_put_contents($plainFile, $plain, FILE_APPEND | LOCK_EX);
}
LogWriter::write($this->logFormat, $jsonFile, $merged, $plainFile, $plain, $this->logger);

TransferStatsStore::clear($this->reqId);
} catch (\Throwable $e) {
Expand Down
20 changes: 5 additions & 15 deletions lib/Http/Client/Middleware/HttpClientLoggerMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ function (ResponseInterface $response) use ($request, $reqHeaders, $reqId) {

if ($immediate) {
if ($this->shouldLog($intStatus)) {
$this->writeImmediate($reqId, $meta, $respHeaders, $handlerStats);
$this->writeImmediate($reqId, $meta, $handlerStats);
}
TransferStatsStore::clear($reqId);
} else {
Expand Down Expand Up @@ -250,12 +250,7 @@ function ($reason) use ($request, $reqHeaders, $reqId) {
];
[$jsonFile, $plainFile] = LogPathHelper::getPathsFromMeta($metaForPaths, $this->logBaseDir, $reqId);

if (in_array($this->logFormat, ['json', 'both'], true)) {
@file_put_contents($jsonFile, json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL, FILE_APPEND | LOCK_EX);
}
if (in_array($this->logFormat, ['plain', 'both'], true)) {
@file_put_contents($plainFile, $plain, FILE_APPEND | LOCK_EX);
}
LogWriter::write($this->logFormat, $jsonFile, $entry, $plainFile, $plain, $this->logger);

TransferStatsStore::clear($reqId);
} catch (\Throwable $e) {
Expand All @@ -271,7 +266,7 @@ function ($reason) use ($request, $reqHeaders, $reqId) {
};
}

private function writeImmediate(string $reqId, array $meta, array $respHeaders, ?array $handlerStats): void {
private function writeImmediate(string $reqId, array $meta, ?array $handlerStats): void {
try {
$compressed = null;
$encoding = 'none';
Expand All @@ -280,7 +275,7 @@ private function writeImmediate(string $reqId, array $meta, array $respHeaders,
$compressed = (int)round($handlerStats['size_download']);
}

$compact = $this->compactHeaders($respHeaders);
$compact = $meta['responseHeaders'];

foreach ($compact as $k => $v) {
$lk = strtolower($k);
Expand Down Expand Up @@ -358,12 +353,7 @@ private function writeImmediate(string $reqId, array $meta, array $respHeaders,

[$jsonFile, $plainFile] = LogPathHelper::getPathsFromMeta($meta, $this->logBaseDir, $reqId);

if (in_array($this->logFormat, ['json', 'both'], true)) {
@file_put_contents($jsonFile, json_encode($merged, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL, FILE_APPEND | LOCK_EX);
}
if (in_array($this->logFormat, ['plain', 'both'], true)) {
@file_put_contents($plainFile, $plain, FILE_APPEND | LOCK_EX);
}
LogWriter::write($this->logFormat, $jsonFile, $merged, $plainFile, $plain, $this->logger);
} catch (\Throwable $e) {
$this->logger->debug('HttpClientLoggerMiddleware: writeImmediate failed: ' . $e->getMessage());
}
Expand Down
49 changes: 49 additions & 0 deletions lib/Http/Client/Middleware/LogWriter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 [ernolf] Raphael Gradenwitz <raphael.gradenwitz@googlemail.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

declare(strict_types=1);

namespace OCA\AdminAuditHttpClient\Http\Client\Middleware;

use Psr\Log\LoggerInterface;

/**
* Single write path for all log sinks. Appends with an exclusive lock and
* reports the first failed write per process as a warning — without that, a
* full or unwritable log directory would go entirely unnoticed.
*/
class LogWriter {
private static bool $failureLogged = false;

public static function write(
string $format,
string $jsonFile,
array $entry,
string $plainFile,
string $plainLine,
LoggerInterface $logger,
): void {
if (in_array($format, ['json', 'both'], true)) {
self::append($jsonFile, json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL, $logger);
}
if (in_array($format, ['plain', 'both'], true)) {
self::append($plainFile, $plainLine, $logger);
}
}

private static function append(string $file, string $line, LoggerInterface $logger): void {
if (@file_put_contents($file, $line, FILE_APPEND | LOCK_EX) !== false) {
return;
}
if (!self::$failureLogged) {
self::$failureLogged = true;
$logger->warning(
'admin_audit_http_client: could not write to ' . $file . ' - further write failures are not reported'
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ public function testWriteImmediateOmitsDecompressedBytesAndRatio(): void {
];

try {
$this->invokePrivate($mw, 'writeImmediate', ['req-wi-1', $meta, [], ['size_download' => 10]]);
$this->invokePrivate($mw, 'writeImmediate', ['req-wi-1', $meta, ['size_download' => 10]]);

$lines = file($dir . '/example.com.json', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$this->assertNotFalse($lines);
Expand Down
65 changes: 65 additions & 0 deletions tests/unit/Http/Client/Middleware/LogWriterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

/*
* SPDX-FileCopyrightText: 2026 [ernolf] Raphael Gradenwitz <raphael.gradenwitz@googlemail.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\AdminAuditHttpClient\Tests\Unit\Http\Client\Middleware;

use OCA\AdminAuditHttpClient\Http\Client\Middleware\LogWriter;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

class LogWriterTest extends TestCase {
private string $baseDir;

protected function setUp(): void {
parent::setUp();
$this->baseDir = sys_get_temp_dir() . '/aahc-writer-' . bin2hex(random_bytes(4));
mkdir($this->baseDir);
}

protected function tearDown(): void {
foreach (glob($this->baseDir . '/*') ?: [] as $file) {
@unlink($file);
}
@rmdir($this->baseDir);
parent::tearDown();
}

public function testWritesJsonAndPlainForFormatBoth(): void {
$json = $this->baseDir . '/host.json';
$plain = $this->baseDir . '/host.log';

LogWriter::write('both', $json, ['a' => 1], $plain, "plain line\n", new NullLogger());

$this->assertSame('{"a":1}' . PHP_EOL, file_get_contents($json));
$this->assertSame("plain line\n", file_get_contents($plain));
}

public function testRespectsFormatSelection(): void {
$json = $this->baseDir . '/host.json';
$plain = $this->baseDir . '/host.log';

LogWriter::write('json', $json, ['a' => 1], $plain, "plain line\n", new NullLogger());

$this->assertFileExists($json);
$this->assertFileDoesNotExist($plain);
}

public function testWarnsExactlyOnceOnWriteFailure(): void {
$blocker = $this->baseDir . '/blocker';
touch($blocker);

$logger = $this->createMock(LoggerInterface::class);
// Both appends fail (paths below a regular file); once() proves the
// second failure is suppressed.
$logger->expects($this->once())->method('warning');

LogWriter::write('both', $blocker . '/x.json', ['a' => 1], $blocker . '/x.log', "line\n", $logger);
}
}
Loading