Skip to content

Commit 78ea8d7

Browse files
committed
feat: Add data validation methods for metrics in ClickHouse adapter
1 parent da10c56 commit 78ea8d7

1 file changed

Lines changed: 186 additions & 6 deletions

File tree

src/Usage/Adapter/ClickHouse.php

Lines changed: 186 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,104 @@ protected function getColumnDefinition(string $id): string
560560
return "{$escapedId} {$type}";
561561
}
562562

563+
/**
564+
* Validate data format against attribute metadata.
565+
*
566+
* @param array<string, mixed> $data
567+
* @throws Exception
568+
*/
569+
private function validateDataFormat(array $data): void
570+
{
571+
$attributes = $this->getAttributes();
572+
573+
foreach ($attributes as $attribute) {
574+
/** @var string $attrId */
575+
$attrId = $attribute['$id'];
576+
$required = $attribute['required'] ?? false;
577+
$type = $attribute['type'] ?? 'string';
578+
/** @var int $size */
579+
$size = $attribute['size'] ?? 0;
580+
581+
// Check if required attribute is present
582+
if ($required && !isset($data[$attrId])) {
583+
throw new Exception("Required attribute '{$attrId}' is missing");
584+
}
585+
586+
// Skip validation if not present and not required
587+
if (!isset($data[$attrId])) {
588+
continue;
589+
}
590+
591+
$value = $data[$attrId];
592+
593+
// Special handling for tags: accept array (will be JSON-encoded)
594+
if ($attrId === 'tags') {
595+
if (!is_array($value)) {
596+
throw new Exception("Attribute '{$attrId}' must be an array, got " . gettype($value));
597+
}
598+
continue;
599+
}
600+
601+
// Validate based on attribute type
602+
match ($type) {
603+
'string' => $this->validateStringAttribute($attrId, $value, $size),
604+
'integer' => $this->validateIntegerAttribute($attrId, $value),
605+
'datetime' => $this->validateDatetimeAttribute($attrId, $value),
606+
default => null,
607+
};
608+
}
609+
}
610+
611+
/**
612+
* Validate string attribute value.
613+
*
614+
* @throws Exception
615+
*/
616+
private function validateStringAttribute(string $attrId, mixed $value, int $size): void
617+
{
618+
if (!is_string($value)) {
619+
throw new Exception("Attribute '{$attrId}' must be a string, got " . gettype($value));
620+
}
621+
622+
if ($size > 0 && strlen($value) > $size) {
623+
throw new Exception("Attribute '{$attrId}' exceeds maximum size of {$size} characters");
624+
}
625+
}
626+
627+
/**
628+
* Validate integer attribute value.
629+
*
630+
* @throws Exception
631+
*/
632+
private function validateIntegerAttribute(string $attrId, mixed $value): void
633+
{
634+
if (!is_int($value)) {
635+
throw new Exception("Attribute '{$attrId}' must be an integer, got " . gettype($value));
636+
}
637+
}
638+
639+
/**
640+
* Validate datetime attribute value.
641+
*
642+
* @throws Exception
643+
*/
644+
private function validateDatetimeAttribute(string $attrId, mixed $value): void
645+
{
646+
if ($value instanceof \DateTime) {
647+
return; // Valid DateTime object
648+
}
649+
650+
if (!is_string($value)) {
651+
throw new Exception("Attribute '{$attrId}' must be a DateTime object or string, got " . gettype($value));
652+
}
653+
654+
try {
655+
new \DateTime($value);
656+
} catch (\Exception $e) {
657+
throw new Exception("Attribute '{$attrId}' is not a valid datetime string: {$e->getMessage()}");
658+
}
659+
}
660+
563661
/**
564662
* Log a usage metric.
565663
*
@@ -569,10 +667,38 @@ protected function getColumnDefinition(string $id): string
569667
*/
570668
public function log(string $metric, int $value, string $period = Usage::PERIOD_1H, array $tags = []): bool
571669
{
670+
// Validate period
572671
if (!isset(Usage::PERIODS[$period])) {
573672
throw new \InvalidArgumentException('Invalid period. Allowed: ' . implode(', ', array_keys(Usage::PERIODS)));
574673
}
575674

675+
// Validate metric and value
676+
if (empty($metric)) {
677+
throw new Exception('Metric cannot be empty');
678+
}
679+
680+
if (strlen($metric) > 255) {
681+
throw new Exception('Metric exceeds maximum size of 255 characters');
682+
}
683+
684+
if ($value < 0) {
685+
throw new Exception('Value cannot be negative');
686+
}
687+
688+
// Validate tags format
689+
if (!is_array($tags)) {
690+
throw new Exception('Tags must be an array');
691+
}
692+
693+
// Validate complete data structure
694+
$data = [
695+
'metric' => $metric,
696+
'value' => $value,
697+
'period' => $period,
698+
'tags' => $tags,
699+
];
700+
$this->validateDataFormat($data);
701+
576702
$id = uniqid('', true);
577703
$now = new \DateTime();
578704
$timestamp = $this->formatDateTime($now);
@@ -642,6 +768,66 @@ public function logBatch(array $metrics): bool
642768
return true;
643769
}
644770

771+
// Validate all metrics before processing
772+
foreach ($metrics as $index => $metricData) {
773+
try {
774+
// Validate required fields exist
775+
if (!isset($metricData['metric'])) {
776+
throw new Exception("Metric #{$index}: 'metric' is required");
777+
}
778+
if (!isset($metricData['value'])) {
779+
throw new Exception("Metric #{$index}: 'value' is required");
780+
}
781+
782+
$metric = $metricData['metric'];
783+
$value = $metricData['value'];
784+
$period = $metricData['period'] ?? Usage::PERIOD_1H;
785+
786+
// Validate types
787+
if (!is_string($metric)) {
788+
throw new Exception("Metric #{$index}: 'metric' must be a string, got " . gettype($metric));
789+
}
790+
if (!is_int($value)) {
791+
throw new Exception("Metric #{$index}: 'value' must be an integer, got " . gettype($value));
792+
}
793+
if (!is_string($period)) {
794+
throw new Exception("Metric #{$index}: 'period' must be a string, got " . gettype($period));
795+
}
796+
797+
// Validate metric and value constraints
798+
if (empty($metric)) {
799+
throw new Exception("Metric #{$index}: 'metric' cannot be empty");
800+
}
801+
if (strlen($metric) > 255) {
802+
throw new Exception("Metric #{$index}: 'metric' exceeds maximum size of 255 characters");
803+
}
804+
if ($value < 0) {
805+
throw new Exception("Metric #{$index}: 'value' cannot be negative");
806+
}
807+
808+
// Validate period
809+
if (!isset(Usage::PERIODS[$period])) {
810+
throw new Exception("Metric #{$index}: Invalid period '{$period}'. Allowed: " . implode(', ', array_keys(Usage::PERIODS)));
811+
}
812+
813+
// Validate tags if provided
814+
if (isset($metricData['tags']) && !is_array($metricData['tags'])) {
815+
throw new Exception("Metric #{$index}: 'tags' must be an array, got " . gettype($metricData['tags']));
816+
}
817+
818+
// Validate complete data structure against attributes
819+
$data = [
820+
'metric' => $metric,
821+
'value' => $value,
822+
'period' => $period,
823+
'tags' => $metricData['tags'] ?? [],
824+
];
825+
$this->validateDataFormat($data);
826+
} catch (Exception $e) {
827+
throw new Exception($e->getMessage());
828+
}
829+
}
830+
645831
$tableName = $this->getTableName();
646832
$escapedDatabaseAndTable = $this->escapeIdentifier($this->database) . '.' . $this->escapeIdentifier($tableName);
647833

@@ -661,18 +847,12 @@ public function logBatch(array $metrics): bool
661847
foreach ($metrics as $metricData) {
662848
$period = $metricData['period'] ?? Usage::PERIOD_1H;
663849

664-
if (!isset(Usage::PERIODS[$period])) {
665-
throw new \InvalidArgumentException('Invalid period. Allowed: ' . implode(', ', array_keys(Usage::PERIODS)));
666-
}
667-
668850
$id = uniqid('', true);
669851
$now = new \DateTime();
670852
$timestamp = $this->formatDateTime($now);
671853

672854
$metric = $metricData['metric'];
673855
$value = $metricData['value'];
674-
assert(is_string($metric));
675-
assert(is_int($value));
676856

677857
$valuePlaceholders = [];
678858

0 commit comments

Comments
 (0)