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
11 changes: 7 additions & 4 deletions .github/workflows/publish-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:

- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-php-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }}
Expand All @@ -48,6 +48,9 @@ jobs:
- name: Run PHP CodeSniffer
run: composer run-script cs

- name: Run PHPUnit tests
run: composer run-script test

deploy:
needs: test
runs-on: ubuntu-latest
Expand All @@ -70,16 +73,16 @@ jobs:
id: create_tag
run: |
VERSION=$(php -r "echo json_decode(file_get_contents('composer.json'))->version;")
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "Tag v$VERSION already exists"
echo "tag_exists=true" >> $GITHUB_OUTPUT
echo "tag_exists=true" >> "$GITHUB_OUTPUT"
else
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git tag "v$VERSION"
git push origin "v$VERSION"
echo "tag_exists=false" >> $GITHUB_OUTPUT
echo "tag_exists=false" >> "$GITHUB_OUTPUT"
fi

- name: Trigger Packagist update
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ $metrics->mutate('login_count', 1); // Increment by 1
$metrics->mutate('error_count', -1); // Decrement by 1
```

### Apps

A domain has one API key, the domain key, shared by all apps of that domain, for example a desktop app, an API and a landing page.
Set `app` to name this one, so the dashboard can filter logs and metrics by app:

```php
$logdash = Logdash::create([
'apiKey' => 'your-domain-key',
'app' => 'api'
]);
```

Every log and metric sent by this instance carries the app.
Older service keys ignore `app`.

### Framework Integration

#### Laravel
Expand Down Expand Up @@ -145,6 +160,7 @@ Creates a new Logdash instance.
- `apiKey` (string, optional): Your Logdash API key. If not provided, only local logging will work
- `host` (string, optional): Logdash API host. Defaults to `https://api.logdash.io`
- `verbose` (bool, optional): Enable verbose internal logging. Defaults to `false`
- `app` (string, optional): Name of this app within its domain, sent with every log and metric. See [Apps](#apps)

### Logger

Expand Down
5 changes: 3 additions & 2 deletions src/Logdash.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ private function __construct(
}

/**
* @param array{apiKey?:string, host?:string, verbose?:bool}|null $params
* @param array{apiKey?:string, host?:string, verbose?:bool, app?:string}|null $params
*/
public static function create(?array $params = null): self
{
$initParams = new InitializationParams(
apiKey: $params['apiKey'] ?? null,
host: $params['host'] ?? 'https://api.logdash.io',
verbose: $params['verbose'] ?? false
verbose: $params['verbose'] ?? false,
app: $params['app'] ?? null
);

$requiredParams = $initParams->toRequired();
Expand Down
1 change: 1 addition & 0 deletions src/Metrics/Metrics.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ private function sendMetric(string $name, float $value, MetricOperation $operati
'name' => $name,
'value' => $value,
'operation' => $operation->value,
...($this->params->app === null ? [] : ['app' => $this->params->app]),
],
'timeout' => 5,
]);
Expand Down
1 change: 1 addition & 0 deletions src/Sync/HttpLogSync.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public function send(string $message, LogLevel $level, string $createdAt): void
'level' => $level->value,
'createdAt' => $createdAt,
'sequenceNumber' => $this->sequenceNumber++,
...($this->params->app === null ? [] : ['app' => $this->params->app]),
],
'timeout' => 5,
]);
Expand Down
8 changes: 6 additions & 2 deletions src/Types/InitializationParams.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@ class InitializationParams
public function __construct(
public readonly ?string $apiKey = null,
public readonly string $host = 'https://api.logdash.io',
public readonly bool $verbose = false
public readonly bool $verbose = false,
public readonly ?string $app = null
) {
}

public function toRequired(): RequiredInitializationParams
{
$app = trim($this->app ?? '');

return new RequiredInitializationParams(
apiKey: $this->apiKey ?? '',
host: $this->host,
verbose: $this->verbose
verbose: $this->verbose,
app: $app === '' ? null : $app
);
}
}
3 changes: 2 additions & 1 deletion src/Types/RequiredInitializationParams.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ class RequiredInitializationParams
public function __construct(
public readonly string $apiKey,
public readonly string $host,
public readonly bool $verbose
public readonly bool $verbose,
public readonly ?string $app = null
) {
}
}
119 changes: 119 additions & 0 deletions tests/LogdashTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,50 @@

use Logdash\Logdash;
use Logdash\Types\LogLevel;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class LogdashTest extends TestCase
{
/** @var resource */
private static $server;
private static string $host;
private static string $requestsFile;

public static function setUpBeforeClass(): void
{
// A local server that records every request the SDK sends to it, see tests/server.php
self::$requestsFile = tempnam(sys_get_temp_dir(), 'logdash');
$socket = stream_socket_server('tcp://127.0.0.1:0');
$address = stream_socket_get_name($socket, false);
fclose($socket);

self::$host = "http://{$address}";
self::$server = proc_open(
[PHP_BINARY, '-S', $address, __DIR__ . '/server.php'],
[['null'], ['null'], ['null']],
$pipes,
null,
['LOGDASH_REQUESTS_FILE' => self::$requestsFile] + getenv()
);

for ($attempt = 0; $attempt < 100 && @stream_socket_client("tcp://{$address}") === false; $attempt++) {
usleep(50_000);
}
}

public static function tearDownAfterClass(): void
{
proc_terminate(self::$server);
proc_close(self::$server);
unlink(self::$requestsFile);
}

protected function setUp(): void
{
file_put_contents(self::$requestsFile, '');
}

public function testCreateWithoutApiKey(): void
{
$logdash = Logdash::create();
Expand Down Expand Up @@ -72,4 +112,83 @@ public function testLogLevelEnum(): void
$this->assertEquals('debug', LogLevel::DEBUG->value);
$this->assertEquals('silly', LogLevel::SILLY->value);
}

#[DataProvider('apps')]
public function testLogsAndMetricsCarryApp(string $app, string $sentApp): void
{
$logdash = Logdash::create([
'apiKey' => 'test-api-key',
'host' => self::$host,
'app' => $app
]);

$logdash->logger()->info('Test info');
$logdash->logger()->error('Test error');
$logdash->metrics()->set('test_metric', 42.5);
$logdash->metrics()->mutate('test_counter', 1);

$this->assertSame([$sentApp, $sentApp], array_column($this->receivedBodies('POST /logs'), 'app'));
$this->assertSame([$sentApp, $sentApp], array_column($this->receivedBodies('PUT /metrics'), 'app'));
}

/**
* @return array<string, array{string, string}>
*/
public static function apps(): array
{
return [
'app' => ['api', 'api'],
'padded app' => [' api ', 'api'],
];
}

/**
* @param array{app?: string} $appParams
*/
#[DataProvider('unsetApps')]
public function testLogsAndMetricsOmitAppWhenUnset(array $appParams): void
{
$logdash = Logdash::create(['apiKey' => 'test-api-key', 'host' => self::$host] + $appParams);

$logdash->logger()->info('Test info');
$logdash->metrics()->set('test_metric', 42.5);

$logs = $this->receivedBodies('POST /logs');
$metrics = $this->receivedBodies('PUT /metrics');

$this->assertCount(1, $logs);
$this->assertArrayNotHasKey('app', $logs[0]);
$this->assertCount(1, $metrics);
$this->assertArrayNotHasKey('app', $metrics[0]);
}

/**
* @return array<string, array{array{app?: string}}>
*/
public static function unsetApps(): array
{
return [
'no app' => [[]],
'empty app' => [['app' => '']],
'blank app' => [['app' => ' ']],
];
}

/**
* @return list<array<string, mixed>> JSON bodies the local server received for $request, such as "POST /logs"
*/
private function receivedBodies(string $request): array
{
$bodies = [];

foreach (file(self::$requestsFile, FILE_IGNORE_NEW_LINES) as $line) {
[$method, $path, $body] = explode(' ', $line, 3);

if ("{$method} {$path}" === $request) {
$bodies[] = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
}
}

return $bodies;
}
}
10 changes: 10 additions & 0 deletions tests/server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

// Router for the PHP built-in server that LogdashTest starts: appends "METHOD /path body" for every request
file_put_contents(
getenv('LOGDASH_REQUESTS_FILE'),
"{$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']} " . file_get_contents('php://input') . PHP_EOL,
FILE_APPEND
);
Loading