From 5bc1c1d39bd3c9adae00e5f85e871dde9fda70ec Mon Sep 17 00:00:00 2001 From: Valentin Date: Mon, 15 Feb 2016 16:37:55 +0300 Subject: [PATCH] INIT --- .gitignore | 1 + composer.json | 18 ++ .../Controllers/SnapshotsController.php | 260 ++++++++++++++++++ source/Snapshotter/Database/Aggregation.php | 128 +++++++++ source/Snapshotter/Database/Snapshot.php | 137 +++++++++ .../Database/Sources/AggregationSource.php | 68 +++++ .../Database/Sources/SnapshotSource.php | 61 ++++ .../Snapshotter/Debug/AggregatedSnapshot.php | 129 +++++++++ .../Snapshotter/Models/AggregationService.php | 103 +++++++ source/Snapshotter/Models/SnapshotService.php | 64 +++++ source/Snapshotter/Models/Statistics.php | 29 ++ source/SnapshotterModule.php | 37 +++ source/views/aggregation.dark.php | 139 ++++++++++ source/views/list.dark.php | 79 ++++++ source/views/snapshot.dark.php | 61 ++++ 15 files changed, 1314 insertions(+) create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 source/Snapshotter/Controllers/SnapshotsController.php create mode 100644 source/Snapshotter/Database/Aggregation.php create mode 100644 source/Snapshotter/Database/Snapshot.php create mode 100644 source/Snapshotter/Database/Sources/AggregationSource.php create mode 100644 source/Snapshotter/Database/Sources/SnapshotSource.php create mode 100644 source/Snapshotter/Debug/AggregatedSnapshot.php create mode 100644 source/Snapshotter/Models/AggregationService.php create mode 100644 source/Snapshotter/Models/SnapshotService.php create mode 100644 source/Snapshotter/Models/Statistics.php create mode 100644 source/SnapshotterModule.php create mode 100644 source/views/aggregation.dark.php create mode 100644 source/views/list.dark.php create mode 100644 source/views/snapshot.dark.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..61f1357 --- /dev/null +++ b/composer.json @@ -0,0 +1,18 @@ +{ + "name": "spiral/snapshotter", + "description": "Snapshot component with ability to view, delete and aggregate snapshots.", + "authors": [ + { + "name": "Valentin V / vvval", + "email": "vintsukevich@gmail.com" + } + ], + "require": { + "spiral/vault": "*" + }, + "autoload": { + "psr-4": { + "Spiral\\": "source/" + } + } +} \ No newline at end of file diff --git a/source/Snapshotter/Controllers/SnapshotsController.php b/source/Snapshotter/Controllers/SnapshotsController.php new file mode 100644 index 0000000..361231a --- /dev/null +++ b/source/Snapshotter/Controllers/SnapshotsController.php @@ -0,0 +1,260 @@ +views->render('keeper:vault/snapshots/list', [ + 'source' => $source->findWithSnapshots()->orderBy('last_occurred_time', 'DESC'), + 'lastSnapshot' => $source->findLast(), + 'statistics' => $statistics + ]); + } + + /** + * @param string $id + * @param AggregationService $aggregationService + * @param SnapshotSource $snapshotSource + * @return mixed + */ + public function editAction( + $id, + AggregationService $aggregationService, + SnapshotSource $snapshotSource + ) { + //todo graph + /** + * @var Aggregation $aggregation + */ + $aggregation = $aggregationService->getSource()->findByPK($id); + if (empty($aggregation)) { + throw new NotFoundException; + } + + $this->authorize('view', compact('aggregation')); + + return $this->views->render('keeper:vault/snapshots/aggregation', [ + 'source' => $snapshotSource->findStored($aggregation)->orderBy('id', 'DESC'), + 'aggregation' => $aggregation + ]); + } + + /** + * @param string $id + * @param AggregationSource $source + * @return array + */ + public function suppressAction($id, AggregationSource $source) + { + /** + * @var Aggregation $aggregation + */ + $aggregation = $source->findByPK($id); + if (empty($aggregation)) { + throw new NotFoundException; + } + + $this->authorize('edit', compact('aggregation')); + + $aggregation->setSuppression($this->input->data('suppression', false)); + $source->save($aggregation); + + return [ + 'status' => 200, + 'message' => $this->say('Suppression status updated.') + ]; + } + + /** + * @param string $id + * @param SnapshotSource $source + * @return mixed + */ + public function snapshotAction($id, SnapshotSource $source) + { + $snapshot = $source->findByPK($id); + if (empty($snapshot)) { + throw new NotFoundException; + } + + $this->authorize('view', compact('snapshot')); + + return $this->views->render('keeper:vault/snapshots/snapshot', compact('snapshot')); + } + + /** + * @param string $id + * @param SnapshotSource $source + * @return string + */ + public function iframeAction($id, SnapshotSource $source) + { + $snapshot = $source->findByPK($id); + if (empty($snapshot)) { + throw new NotFoundException; + } + + $this->authorize('view', compact('snapshot')); + + try { + return file_get_contents($snapshot->filename); + } catch (\Exception $exception) { + throw new NotFoundException; + } + } + + /** + * @param AggregationService $aggregationService + * @param AggregationSource $aggregationSource + * @param SnapshotSource $snapshotSource + * @return array + */ + public function removeAllAction( + AggregationService $aggregationService, + AggregationSource $aggregationSource, + SnapshotSource $snapshotSource + ) { + $this->authorize('remove'); + + foreach ($aggregationSource->find() as $aggregation) { + $countDeleted = 0; + if (!empty($snapshotSource->findStored($aggregation)->count())) { + foreach ($snapshotSource->findStored($aggregation) as $snapshot) { + $countDeleted++; + $snapshotSource->delete($snapshot); + } + } + + if (!empty($countDeleted)) { + $aggregationService->deleteSnapshots($aggregation, $countDeleted); + $aggregationSource->save($aggregation); + } + } + + return [ + 'status' => 200, + 'message' => $this->say('Snapshot deleted.'), + 'action' => [ + 'redirect' => $this->vault->uri('snapshots') + ] + ]; + } + + /** + * @param string $id + * @param AggregationService $aggregationService + * @param SnapshotSource $snapshotSource + * @return array + */ + public function removeSnapshotsAction( + $id, + AggregationService $aggregationService, + SnapshotSource $snapshotSource + ) { + /** + * @var Aggregation $aggregation + */ + $aggregation = $aggregationService->getSource()->findByPK($id); + if (empty($aggregation)) { + throw new NotFoundException; + } + + $this->authorize('remove', compact('aggregation')); + + $countDeleted = 0; + if (!empty($snapshotSource->findStored($aggregation)->count())) { + foreach ($snapshotSource->findStored($aggregation) as $snapshot) { + $countDeleted++; + $snapshotSource->delete($snapshot); + } + } + + if (!empty($countDeleted)) { + $aggregationService->deleteSnapshots($aggregation, $countDeleted); + $aggregationService->getSource()->save($aggregation); + } + + return [ + 'status' => 200, + 'message' => $this->say('Snapshot deleted.'), + 'action' => [ + 'redirect' => $this->vault->uri('snapshots:edit', ['id' => $aggregation->id]) + ] + ]; + } + + /** + * @param string $id + * @param SnapshotSource $snapshotSource + * @param AggregationService $aggregationService + * @return array + */ + public function removeSnapshotAction( + $id, + SnapshotSource $snapshotSource, + AggregationService $aggregationService + ) { + /** + * @var Snapshot $snapshot + * @var Aggregation $aggregation + */ + $snapshot = $snapshotSource->findByPK($id); + if (empty($snapshot)) { + throw new NotFoundException; + } + + if (!$snapshot->stored()) { + throw new ForbiddenException; + } + + $aggregation = $aggregationService->getSource()->findBySnapshot($snapshot); + if (empty($aggregation)) { + throw new NotFoundException; + } + + $this->authorize('remove', compact('aggregation', 'snapshot')); + + $aggregationService->deleteSnapshots($aggregation, 1); + $aggregationService->getSource()->save($aggregation); + + $snapshotSource->delete($snapshot); + + return [ + 'status' => 200, + 'message' => $this->say('Snapshot deleted.'), + 'action' => [ + 'redirect' => $this->vault->uri('snapshots:edit', ['id' => $aggregation->id]) + ] + ]; + } +} \ No newline at end of file diff --git a/source/Snapshotter/Database/Aggregation.php b/source/Snapshotter/Database/Aggregation.php new file mode 100644 index 0000000..03dc8b6 --- /dev/null +++ b/source/Snapshotter/Database/Aggregation.php @@ -0,0 +1,128 @@ + 'primary', + 'last_occurred_time' => 'timestamp', + 'suppression' => 'bool', + + //exception teaser + 'exception_hash' => 'string', + 'exception_teaser' => 'string', + + //counters + 'count_occurred' => 'int', + 'count_stored' => 'int', + 'count_suppressed' => 'int', + 'count_deleted' => 'int' + ]; + + /** + * {@inheritdoc} + */ + protected $indexes = [ + [self::UNIQUE, 'exception_hash'], + [self::INDEX, 'exception_teaser'] + ]; + + /** + * {@inheritdoc} + */ + protected $defaults = [ + 'suppression' => false + ]; + + /** + * {@inheritdoc} + */ + protected $accessors = [ + 'count_occurred' => AtomicNumber::class, + 'count_suppressed' => AtomicNumber::class, + 'count_deleted' => AtomicNumber::class, + 'count_stored' => AtomicNumber::class + ]; + + /** + * @return bool + */ + public function isSuppressionEnabled() + { + return !empty($this->suppression); + } + + /** + * @param bool $relative + * @return SqlTimestamp|string + */ + public function whenFirst($relative = false) + { + return $this->when($this->time_created, $relative); + } + + /** + * @param bool $relative + * @return SqlTimestamp|string + */ + public function whenLast($relative = false) + { + return $this->when($this->last_occurred_time, $relative); + } + + /** + * @param SqlTimestamp $timestamp + * @param bool $relative + * @return mixed + */ + private function when($timestamp, $relative) + { + if (!empty($relative)) { + return $timestamp->diffForHumans(Carbon::now()); + } + + return $timestamp; + } + + /** + * @param $suppression + */ + public function setSuppression($suppression) + { + $this->suppression = $suppression; + } +} \ No newline at end of file diff --git a/source/Snapshotter/Database/Snapshot.php b/source/Snapshotter/Database/Snapshot.php new file mode 100644 index 0000000..86ea6b3 --- /dev/null +++ b/source/Snapshotter/Database/Snapshot.php @@ -0,0 +1,137 @@ + 'primary', + 'filename' => 'string', + 'status' => 'enum(stored,deleted,suppressed)', + + //relations +// 'aggregation_id' => 'bigint', + 'aggregation' => [ + self::BELONGS_TO => Aggregation::class, + self::INVERSE => [self::HAS_MANY, 'snapshots'] + ], + + //exception fields + 'exception_hash' => 'string', + 'exception_teaser' => 'string', + 'exception_classname' => 'string', + 'exception_message' => 'string', + 'exception_line' => 'int', + 'exception_file' => 'string', + 'exception_code' => 'int' + ]; + + /** + * {@inheritdoc} + */ + protected $indexes = [ + [self::INDEX, 'exception_hash'] + ]; + + /** + * {@inheritdoc} + */ + protected $defaults = [ + 'status' => 'stored' + ]; + + /** + * @param bool $relative + * @return mixed + */ + public function when($relative = false) + { + if (!empty($relative)) { + return $this->time_created->diffForHumans(Carbon::now()); + } + + return $this->time_created; + } + + /** + * @return bool + */ + public function deleted() + { + return $this->status == 'deleted'; + } + + /** + * @return bool + */ + public function suppressed() + { + return $this->status == 'suppressed'; + } + + /** + * @return bool + */ + public function stored() + { + return $this->status == 'stored'; + } + + /** + * + */ + public function setSuppressed() + { + $this->status = 'suppressed'; + } + + /** + * + */ + public function setDeleted() + { + $this->status = 'deleted'; + } + + /** + * @param bool $format + * @return int|string + */ + public function filesize($format = false) + { + $filesize = filesize($this->filename); + + if ($format) { + return Strings::bytes($filesize); + } + + return $filesize; + } +} \ No newline at end of file diff --git a/source/Snapshotter/Database/Sources/AggregationSource.php b/source/Snapshotter/Database/Sources/AggregationSource.php new file mode 100644 index 0000000..33c9f8b --- /dev/null +++ b/source/Snapshotter/Database/Sources/AggregationSource.php @@ -0,0 +1,68 @@ +find()->where(compact('exception_hash'))->findOne(); + } + + /** + * @return \Spiral\ORM\Entities\RecordSelector + */ + public function findWithSnapshots() + { + return $this->find()->where(['count_stored' => ['>=' => 1]]); + } + + /** + * @param Snapshot $snapshot + * @return null|\Spiral\ORM\RecordEntity + */ + public function findBySnapshot(Snapshot $snapshot) + { + return $this->findByPK($snapshot->aggregation_id); + } + + /** + * @return null|Aggregation + */ + public function findLast() + { + return $this->findWithSnapshots()->orderBy('last_occurred_time', 'DESC')->findOne(); + } + + /** + * @param Aggregation $entity + * @param array $errors + * @return bool + */ + public function save(Aggregation $entity, &$errors = null) + { + if (!$entity->save()) { + $errors = $entity->getErrors(); + + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/source/Snapshotter/Database/Sources/SnapshotSource.php b/source/Snapshotter/Database/Sources/SnapshotSource.php new file mode 100644 index 0000000..2dd6163 --- /dev/null +++ b/source/Snapshotter/Database/Sources/SnapshotSource.php @@ -0,0 +1,61 @@ +save()) { + $errors = $entity->getErrors(); + + return false; + } + + return true; + } + + /** + * @param Snapshot $snapshot + */ + public function delete(Snapshot $snapshot) + { + $snapshot->setDeleted(); + + $this->save($snapshot); + } + + /** + * @param Aggregation|null $aggregation + * @return \Spiral\ORM\Entities\RecordSelector + */ + public function findStored(Aggregation $aggregation = null) + { + $where = [ + 'status' => 'stored' + ]; + + if (!empty($aggregation)) { + $where['aggregation_id'] = $aggregation->id; + } + + return $this->find($where); + } +} \ No newline at end of file diff --git a/source/Snapshotter/Debug/AggregatedSnapshot.php b/source/Snapshotter/Debug/AggregatedSnapshot.php new file mode 100644 index 0000000..388898e --- /dev/null +++ b/source/Snapshotter/Debug/AggregatedSnapshot.php @@ -0,0 +1,129 @@ +config = $this->saturate($config, SnapshotConfig::class); + $this->logger = $this->saturate($logger, LoggerInterface::class); + $this->snapshotService = $this->saturate($snapshotService, SnapshotService::class); + $this->aggregationService = $this->saturate($aggregationService, AggregationService::class); + + parent::__construct($exception, $logger, $config, $files, $views); + } + + /** + * @param $string + * @return string + */ + private function hash($string) + { + return hash('sha256', $string); + } + + /** + * {@inheritdoc} + */ + public function report() + { + $this->logger->error($this->getMessage()); + + if (!$this->config->reportingEnabled()) { + //No need to record anything + return; + } + + $teaser = $this->getMessage(); + $hash = $this->hash($teaser); + $exception = $this->getException(); + $filename = $this->config->snapshotFilename($exception, time()); + + $snapshot = $this->snapshotService->createFromException( + $exception, + $filename, + $teaser, + $hash + ); + + $snapshotSource = $this->snapshotService->getSource(); + $snapshotSource->save($snapshot); + + $aggregation = $this->aggregationService->findOrCreateByHash($hash, $teaser); + + $suppress = $snapshotSource->findStored($aggregation)->count() + ? $aggregation->isSuppressionEnabled() + : false; + + $this->aggregationService->addSnapshot($aggregation, $snapshot, $suppress); + $this->aggregationService->getSource()->save($aggregation); + + $snapshotSource->save($snapshot); + + if ($suppress) { + //No need to create file + return; + } + + $this->saveSnapshot(); + } +} \ No newline at end of file diff --git a/source/Snapshotter/Models/AggregationService.php b/source/Snapshotter/Models/AggregationService.php new file mode 100644 index 0000000..f81cdf5 --- /dev/null +++ b/source/Snapshotter/Models/AggregationService.php @@ -0,0 +1,103 @@ +source = $source; + parent::__construct($container); + } + + /** + * @param $hash + * @param $teaser + * @return null|Aggregation + */ + public function findOrCreateByHash($hash, $teaser) + { + /** + * @var Aggregation $aggregation + */ + $aggregation = $this->source->findByHash($hash); + if (empty($aggregation)) { + $aggregation = $this->source->create([ + 'exception_hash' => $hash, + 'exception_teaser' => $teaser, + ]); + + $this->source->save($aggregation); + } + + return $aggregation; + } + + /** + * @param Aggregation $aggregation + * @param Snapshot $snapshot + * @param bool $suppress + */ + public function addSnapshot(Aggregation $aggregation, Snapshot $snapshot, $suppress = false) + { + $snapshot->aggregation_id = $aggregation->id; + + $aggregation->count_occurred->inc(1); + $aggregation->last_occurred_time = time(); + + if ($suppress) { + $this->suppressSnapshot($aggregation); + $snapshot->setSuppressed(); + } else { + $aggregation->count_stored->inc(1); + } + } + + /** + * @param Aggregation $aggregation + * @param int $inc + */ + public function deleteSnapshots(Aggregation $aggregation, $inc) + { + $aggregation->count_deleted->inc($inc); + $aggregation->count_stored->inc(-$inc); + } + + /** + * @param Aggregation $aggregation + */ + public function suppressSnapshot(Aggregation $aggregation) + { + $aggregation->count_suppressed->inc(1); + } + + /** + * @return AggregationSource + */ + public function getSource() + { + return $this->source; + } +} \ No newline at end of file diff --git a/source/Snapshotter/Models/SnapshotService.php b/source/Snapshotter/Models/SnapshotService.php new file mode 100644 index 0000000..8a7048a --- /dev/null +++ b/source/Snapshotter/Models/SnapshotService.php @@ -0,0 +1,64 @@ +source = $source; + parent::__construct($container); + } + + /** + * @param \Exception $exception + * @param string $filename + * @param string $teaser + * @param string $hash + * @return Snapshot + */ + public function createFromException(\Exception $exception, $filename, $teaser, $hash) + { + $fields = [ + 'exception_hash' => $hash, + 'filename' => $filename, + 'exception_teaser' => $teaser, + 'exception_classname' => get_class($exception), + 'exception_message' => $exception->getMessage(), + 'exception_line' => $exception->getLine(), + 'exception_file' => $exception->getFile(), + 'exception_code' => $exception->getCode(), + ]; + + return $this->source->create($fields); + } + + /** + * @return SnapshotSource + */ + public function getSource() + { + return $this->source; + } +} \ No newline at end of file diff --git a/source/Snapshotter/Models/Statistics.php b/source/Snapshotter/Models/Statistics.php new file mode 100644 index 0000000..c522f1e --- /dev/null +++ b/source/Snapshotter/Models/Statistics.php @@ -0,0 +1,29 @@ +source = $source; + } +} \ No newline at end of file diff --git a/source/SnapshotterModule.php b/source/SnapshotterModule.php new file mode 100644 index 0000000..7d0b19b --- /dev/null +++ b/source/SnapshotterModule.php @@ -0,0 +1,37 @@ +configure('views', 'namespaces', 'spiral/vault', [ + "'snapshotter' => [", + " directory('libraries') . 'spiral/snapshotter/source/views/',", + " /*{{namespaces.snapshotter}}*/", + "]" + ]); + } + + /** + * @param PublisherInterface $publisher + * @param DirectoriesInterface $directories + */ + public function publish(PublisherInterface $publisher, DirectoriesInterface $directories) + { + } +} \ No newline at end of file diff --git a/source/views/aggregation.dark.php b/source/views/aggregation.dark.php new file mode 100644 index 0000000..96d275e --- /dev/null +++ b/source/views/aggregation.dark.php @@ -0,0 +1,139 @@ + + + + + count_stored)) { + ?> + + [[Remove all]] + + + + [[BACK]] + + + + + +

exception_teaser ?>

+ count_stored)) { + ?> +

[[No snapshots stored.]]

+ +

whenLast() ?> (whenLast(true) ?>)

+ +
+
+
+ + uri('snapshots:suppress', [ + 'id' => $aggregation->id + ]); + ?> + +

+ isSuppressionEnabled() ? 'checked' : '' ?>/> + +

[[When enabled, snapshot duplicate files will not be + created, only occurred counter will be increased.]]

+
+ +
+
+
+
+
+ +
+
[[Occurred:]]
+
count_occurred ?>
+ +
[[Suppressed:]]
+
count_suppressed ?>
+ +
[[Removed:]]
+
count_deleted ?>
+
+
+
+
+ +
+ count_occurred->serializeData() > 1) { + ?> +
[[First:]]
+
+ whenFirst() ?> + (whenFirst(true) ?>) +
+ +
[[Last:]]
+
+ whenLast() ?> + (whenLast(true) ?>) +
+
+
+
+
+ + + + + + when() ?> + (when(true) ?>) + + + + + + + + stored()) { + ?> + + + + + + stored()) { + ?> + + [[Remove]] + + + + +
\ No newline at end of file diff --git a/source/views/list.dark.php b/source/views/list.dark.php new file mode 100644 index 0000000..b035364 --- /dev/null +++ b/source/views/list.dark.php @@ -0,0 +1,79 @@ + + + + + + + [[Remove all]] + + + + [[View last]] + + + + + + + +

[[No snapshots occurred.]]

+ +

whenLast() ?> (whenLast(true) ?>)

+

exception_teaser ?>

+ +
+ + + + + whenLast() ?> + (whenLast(true) ?>) + + + + exception_teaser, 100)) ?> + + + + + + + isSuppressionEnabled() ? '[[YES]]' : '[[NO]]' ?> + + isSuppressionEnabled()) { + echo '(' . $entity->count_suppressed . ')'; + } + ?> + + + + + + + + + +
diff --git a/source/views/snapshot.dark.php b/source/views/snapshot.dark.php new file mode 100644 index 0000000..4fcbe80 --- /dev/null +++ b/source/views/snapshot.dark.php @@ -0,0 +1,61 @@ + + + + + + + + + stored()) { + ?> + + [[Remove]] + + + + + [[BACK]] + + + + + +

when() ?> (when(true) ?>)

+

exception_classname ?>

+

filesize(true) ?>

+
+ status) { + case 'suppressed': + ?> +

[[Can't render snapshot - it was suppressed.]]

+ +

[[Can't render snapshot - it was removed.]]

+ + + +
\ No newline at end of file