Initial commit

This commit is contained in:
2025-09-26 12:56:23 +02:00
commit 2e757c674c
12 changed files with 1568 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
<?php
namespace LogDb\LogQuery\Client;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class LogDbClient
{
private HttpClientInterface $client;
public function __construct(
string $server,
?string $token = null,
)
{
$this->client = HttpClient::createForBaseUri($server, [
'headers' => ($token ? [
'x-token' => $token,
] : [])
]);
}
public function queryLogs(array $params): array
{
$query = http_build_query($params);
$result = $this->client->request("GET", "/api/logdb/v1/events?{$query}");
$body = $result->getContent();
$json = json_decode($body);
return $json;
}
public function queryScopes(): array
{
$result = $this->client->request("GET", "/api/logdb/v1/scopes");
$body = $result->getContent();
$json = (array)json_decode($body);
return $json;
}
public function deleteScope(string $scope): void
{
$result = $this->client->request("DELETE", "/api/logdb/v1/scope/{$scope}");
$body = $result->getContent();
$json = json_decode($body);
if ($json === false) {
throw new \Exception("Unable to delete scope {$scope}");
}
}
public function querySources(): array
{
$result = $this->client->request("GET", "/api/logdb/v1/sources");
$body = $result->getContent();
$json = (array)json_decode($body);
return $json;
}
public function deleteSource(string $source): void
{
$result = $this->client->request("DELETE", "/api/logdb/v1/source/{$source}");
$body = $result->getContent();
$json = json_decode($body);
if ($json === false) {
throw new \Exception("Unable to delete source {$scope}");
}
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace LogDb\LogQuery\Command;
use LogDb\LogQuery\Client\LogDbClient;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name:"events", description:"Query events")]
class EventCommand extends Command
{
public function __construct(private readonly LogDbClient $client)
{
parent::__construct();
}
protected function configure()
{
$this
->addOption("short", "S", InputOption::VALUE_NONE, "Only produce one line of output for every event")
->addOption("detail", "d", InputOption::VALUE_NONE, "Include extra detail (if not short output)")
->addOption("json", "j", InputOption::VALUE_NONE, "Dump events as JSON")
->addOption("scope", "s", InputOption::VALUE_REQUIRED, "Query events in scope")
->addOption("source", "r", InputOption::VALUE_REQUIRED, "Query events with source")
->addOption("limit", "l", InputOption::VALUE_REQUIRED, "Set number of results", 25)
->addOption("level", "L", InputOption::VALUE_REQUIRED, "Minimum level of event to return");
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$params = [
'count' => intval($input->getOption('limit')),
'scope' => $input->getOption("scope"),
'source' => $input->getOption("source"),
'level' => $input->getOption("level"),
];
$events = $this->client->queryLogs($params);
if ($input->getOption("json")) {
$output->writeln(\json_encode($events, \JSON_PRETTY_PRINT|\JSON_UNESCAPED_SLASHES));
} elseif ($input->getOption("short")) {
$this->dumpShort($output, $events);
} else {
$detail = $input->getOption("detail");
$this->dumpLong($output, $events, $detail);
}
return self::SUCCESS;
}
private function dumpShort(OutputInterface $output, array $events): void
{
$batchId = null;
foreach ($events as $event) {
if ($batchId && $batchId == ($event->batch ?? null)) {
$head = "\u{251c}\u{2574}";
} elseif ($event->batch ?? null) {
$head = "\u{250c}\u{2500}";
} else {
$head = " ";
}
$batchId = $event->batch ?? null;
$line = sprintf("%s<comment>%s</> <options=bold>%s</>: %s <fg=gray>%s</> (<info>%s</>:<fg=cyan>%s</>)", $head, $event->datetime, $event->level, $event->brief, ($event->context?json_encode($event->context,JSON_UNESCAPED_SLASHES):null), $event->scope, $event->origin);
$output->writeln($line);
}
}
private function dumpLong(OutputInterface $output, array $events, bool $detail): void
{
$bar = function(int $len) { return str_repeat("\u{2500}", $len); };
foreach ($events as $event) {
$output->writeln($bar(3)." <options=bold>Event</> <comment>{$event->uuid}</> (<info>{$event->id}</>) ".$bar(10));
$ev = (array)$event;
$keys = $detail ? array_keys($ev) : [ "datetime", "level", "brief", "scope" ];
foreach ($keys as $k) {
$v = $ev[$k] ?? null;
$output->writeln(sprintf(" <info>%10s</>: %s", $k, strtr(json_encode($v,JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT),["\n"=>"\n "])));
}
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace LogDb\LogQuery\Command;
use LogDb\LogQuery\Client\LogDbClient;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name:"scopes", description:"Enumerate or query scopes")]
class ScopeCommand extends Command
{
public function __construct(private readonly LogDbClient $client)
{
parent::__construct();
}
protected function configure()
{
$this->addOption("delete", null, InputOption::VALUE_REQUIRED, "Delete a scope");
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($scopeToDelete = $input->getOption("delete")) {
try {
$this->client->deleteScope($scopeToDelete);
return self::SUCCESS;
} catch (\Exception $e) {
$output->writeln("<error>{$e->getMessage()}</>");
return self::FAILURE;
}
}
$scopes = $this->client->queryScopes();
$table = new Table($output);
$table->setStyle("box");
$table->setHeaders([ "Scope", "Events", "Latest" ]);
foreach ($scopes as $scope) {
$table->addRow([ $scope->name, $scope->events, $scope->latest ]);
}
$table->render();
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace LogDb\LogQuery\Command;
use LogDb\LogQuery\Client\LogDbClient;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name:"sources", description:"Enumerate or query sources")]
class SourceCommand extends Command
{
public function __construct(private readonly LogDbClient $client)
{
parent::__construct();
}
protected function configure()
{
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$sources = $this->client->querySources();
$table = new Table($output);
$table->setStyle("box");
$table->setHeaders([ "Source", "Events", "Latest" ]);
foreach ($sources as $source) {
$table->addRow([ $source->name, $source->events, $source->latest ]);
}
$table->render();
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace LogDb\LogQuery;
use LogDb\LogQuery\Client\LogDbClient;
use Symfony\Component\Console\Application;
class LogQueryApplication extends Application
{
public function __construct()
{
parent::__construct("LogQuery", "0.0.0");
$server = getenv("LOGQUERY_SERVER") ?: "http://127.0.0.1:7000";
$token = getenv("LOGQUERY_TOKEN") ?: null;
$client = new LogDbClient($server, $token);
$this->add(new Command\EventCommand($client));
$this->add(new Command\ScopeCommand($client));
$this->add(new Command\SourceCommand($client));
}
}

8
src/logquery.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
use LogDb\LogQuery\LogQueryApplication;
require_once __DIR__."/../vendor/autoload.php";
$app = new LogQueryApplication();
$app->run();