Initial commit
This commit is contained in:
commit
d222270fee
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.env
|
||||||
|
vendor
|
||||||
|
.idea
|
||||||
24
composer.json
Normal file
24
composer.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "maker-dave/elastic-push",
|
||||||
|
"description": "Pull remote log files from S3 and inject new lines into local elasticsearch or analysis",
|
||||||
|
"type": "project",
|
||||||
|
"version": "1.0",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.1",
|
||||||
|
"ext-curl": "*",
|
||||||
|
"aws/aws-sdk-php": "^3.314",
|
||||||
|
"vlucas/phpdotenv": "^5.6"
|
||||||
|
},
|
||||||
|
"license": "private",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"MakerDave\\ElasticPush\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "david-fairbanks42",
|
||||||
|
"email": "david@makerdave.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1293
composer.lock
generated
Normal file
1293
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
config/.gitignore
vendored
Normal file
2
config/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
24
main.php
Executable file
24
main.php
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
use MakerDave\ElasticPush\LogSource;
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
chdir(__DIR__);
|
||||||
|
|
||||||
|
$env = Dotenv::createImmutable(__DIR__);
|
||||||
|
$env->safeLoad();
|
||||||
|
|
||||||
|
foreach (new DirectoryIterator(__DIR__ . '/config') as $file) {
|
||||||
|
if ($file->isDot() || $file->isDir()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($file->getExtension() !== 'json') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$worker = new LogSource($file->getPathname());
|
||||||
|
$worker->work();
|
||||||
|
}
|
||||||
226
src/LogSource.php
Normal file
226
src/LogSource.php
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* LogSource.php
|
||||||
|
*
|
||||||
|
* @copyright 2024 Fairbanks Publishing LLC
|
||||||
|
* @license Proprietary
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace MakerDave\ElasticPush;
|
||||||
|
|
||||||
|
use Aws\Exception\AwsException;
|
||||||
|
use Aws\S3\S3Client;
|
||||||
|
use DateTime;
|
||||||
|
use DateTimeZone;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class LogSource
|
||||||
|
*
|
||||||
|
* @author David Fairbanks <david@makerdave.com>
|
||||||
|
* @version 1.0
|
||||||
|
*/
|
||||||
|
class LogSource
|
||||||
|
{
|
||||||
|
protected const BULK_SIZE = 50;
|
||||||
|
|
||||||
|
protected object $config;
|
||||||
|
protected array $files;
|
||||||
|
protected DateTime|null $lastDate = null;
|
||||||
|
protected string|null $lastHash = null;
|
||||||
|
protected bool $lastFound = false;
|
||||||
|
protected array $events;
|
||||||
|
protected int $skipCount = 0;
|
||||||
|
protected int $sendCount = 0;
|
||||||
|
|
||||||
|
protected S3Client $client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $configPath
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected string $configPath
|
||||||
|
) {
|
||||||
|
$config = json_decode(file_get_contents($configPath));
|
||||||
|
if (!is_object($config)) {
|
||||||
|
throw new Exception('Unable to parse log configuration file: ' . $configPath);
|
||||||
|
}
|
||||||
|
$this->config = $config;
|
||||||
|
|
||||||
|
if (isset($this->config->file) && is_string($this->config->file)) {
|
||||||
|
$this->files = [$this->config->file];
|
||||||
|
} elseif (isset($this->config->file) && is_array($this->config->file)) {
|
||||||
|
$this->files = $this->config->file;
|
||||||
|
} elseif (isset($this->config->files) && is_string($this->config->files)) {
|
||||||
|
$this->files = [$this->config->files];
|
||||||
|
} else {
|
||||||
|
$this->files = $this->config->files;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($this->files) > 1) {
|
||||||
|
$this->files = array_unique($this->files);
|
||||||
|
rsort($this->files);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->config->date !== null) {
|
||||||
|
$lastDate = date_create($this->config->date);
|
||||||
|
if ($lastDate === false) {
|
||||||
|
throw new Exception('Unable to parse last date: ' . $configPath);
|
||||||
|
}
|
||||||
|
$lastDate->setTimezone(new DateTimeZone('UTC'));
|
||||||
|
$this->lastDate = $lastDate;
|
||||||
|
}
|
||||||
|
if ($this->config->hash !== null) {
|
||||||
|
$this->lastHash = $this->config->hash;
|
||||||
|
} else {
|
||||||
|
$this->lastFound = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->client = new S3Client([
|
||||||
|
'profile' => 'david-fairbanks42',
|
||||||
|
'version' => 'latest',
|
||||||
|
'region' => 'us-east-1',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function work(): void
|
||||||
|
{
|
||||||
|
foreach ($this->files as $file) {
|
||||||
|
if ($this->getFile($file) === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->processFile($file);
|
||||||
|
unlink($_ENV['TEMP_LOG_STORE_DIR'] . DIRECTORY_SEPARATOR . $file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($this->events)) {
|
||||||
|
$this->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
echo sprintf(
|
||||||
|
"%s\n\tSkipped %d log entries\n\tSent %d log entries\n",
|
||||||
|
basename($this->configPath),
|
||||||
|
$this->skipCount,
|
||||||
|
$this->sendCount
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->config->hash !== null) {
|
||||||
|
file_put_contents($this->configPath, json_encode($this->config));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFile(string $file): bool
|
||||||
|
{
|
||||||
|
if (! file_exists($_ENV['TEMP_LOG_STORE_DIR'])) {
|
||||||
|
mkdir($_ENV['TEMP_LOG_STORE_DIR']);
|
||||||
|
}
|
||||||
|
if (! is_dir($_ENV['TEMP_LOG_STORE_DIR'])) {
|
||||||
|
throw new Exception('Target directory ' . $_ENV['TEMP_LOG_STORE_DIR'] . ' exists but is not a directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->client->getObject([
|
||||||
|
'Bucket' => 'fairbanks-publishing-cloudtrail',
|
||||||
|
'Key' => 'app-logs/' . $file,
|
||||||
|
'SaveAs' => $_ENV['TEMP_LOG_STORE_DIR'] . DIRECTORY_SEPARATOR . $file,
|
||||||
|
]);
|
||||||
|
return true;
|
||||||
|
} catch (AwsException $e) {
|
||||||
|
echo sprintf('ERROR: Unable get log file %s: %s', $file, $e->getMessage());
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function processFile(string $file): void
|
||||||
|
{
|
||||||
|
$fh = fopen($_ENV['TEMP_LOG_STORE_DIR'] . DIRECTORY_SEPARATOR . $file, 'r');
|
||||||
|
|
||||||
|
$lineNumber = 0;
|
||||||
|
while (!feof($fh)) {
|
||||||
|
$lineNumber ++;
|
||||||
|
$line = trim(fgets($fh));
|
||||||
|
if (empty($line)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($line, true);
|
||||||
|
if (! is_array($data)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$date = date_create($data['time']);
|
||||||
|
if ($date === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$date->setTimezone(new DateTimeZone('UTC'));
|
||||||
|
|
||||||
|
if ($this->lastDate !== null && $date < $this->lastDate) {
|
||||||
|
$this->skipCount ++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hash = hash('sha1', '(' . $lineNumber . ') ' . $line);
|
||||||
|
if ($this->lastFound === false && $this->lastDate !== null && $date == $this->lastDate && $hash == $this->lastHash) {
|
||||||
|
$this->lastFound = true;
|
||||||
|
$this->skipCount ++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->lastFound === false && $this->lastDate !== null && $date > $this->lastDate) {
|
||||||
|
$this->lastFound = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->lastFound === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['@timestamp'] = date_format($date, 'c');
|
||||||
|
$data['time'] = date_format($date, 'c');
|
||||||
|
$this->events[] = ['hash' => $hash, 'date' => $data['time'], 'line' => json_encode($data)];
|
||||||
|
$this->sendCount ++;
|
||||||
|
|
||||||
|
$this->config->date = $data['time'];
|
||||||
|
$this->config->hash = $hash;
|
||||||
|
|
||||||
|
if (count($this->events) == self::BULK_SIZE) {
|
||||||
|
$this->send();
|
||||||
|
$this->events = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($fh);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function send(): void
|
||||||
|
{
|
||||||
|
$body = '';
|
||||||
|
foreach ($this->events as $item) {
|
||||||
|
$body .= json_encode(['create' => ['_index' => $this->config->index, '_id' => $item['hash']]]) . "\n";
|
||||||
|
$body .= $item['line'] . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$curl = curl_init();
|
||||||
|
curl_setopt_array($curl, [
|
||||||
|
CURLOPT_URL => $_ENV['ELASTICSEARCH_HOST'] . '/_bulk',
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Authorization: ApiKey ' . $_ENV['ELASTICSEARCH_API_KEY'],
|
||||||
|
],
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_SSL_VERIFYPEER => false,
|
||||||
|
CURLOPT_SSL_VERIFYHOST => false,
|
||||||
|
CURLOPT_USERAGENT => 'MakerDave Log Sender 1.0',
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $body,
|
||||||
|
//CURLOPT_VERBOSE => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resp= curl_exec($curl);
|
||||||
|
curl_close($curl);
|
||||||
|
|
||||||
|
$response = json_decode($resp, true);
|
||||||
|
if ($response['errors']) {
|
||||||
|
echo $resp . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user