Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
978c524
feat(metric): widen EVENT_COLUMNS, add GAUGE_COLUMNS
lohanidamodar May 25, 2026
0002ddd
feat(metric): regenerate event/gauge schemas to match new column set
lohanidamodar May 25, 2026
5c9d7e0
feat(metric): add 18 new typed getters, drop getUserAgent/getTags
lohanidamodar May 25, 2026
cd556c9
refactor(metric): drop tags special-case in validate()
lohanidamodar May 25, 2026
7ebff56
feat(metric): bloom_filter indexes for new filterable columns
lohanidamodar May 25, 2026
321c8d7
feat(clickhouse): widen LowCardinality treatment for bounded vocab co…
lohanidamodar May 25, 2026
c9676c6
feat(clickhouse): widen daily MV with resource/team dimensions
lohanidamodar May 25, 2026
5c044a3
feat(clickhouse): strict tag extraction, drop tags column, normalize …
lohanidamodar May 25, 2026
1e68d6e
feat(database-adapter): apply schema refactor to dev adapter
lohanidamodar May 25, 2026
e29a1c9
test(clickhouse): cover new schema columns, strict tags, normalization
lohanidamodar May 25, 2026
1d67362
test(database-adapter): cover new schema columns and strict tags
lohanidamodar May 25, 2026
90298f6
docs(usage): refresh README and Metric docs for new schema
lohanidamodar May 25, 2026
cae2dcf
chore(usage): apply formatter and refresh PHPStan baseline
lohanidamodar May 25, 2026
3639dc9
fix: bump region column size to 64; share extraction helper across ad…
lohanidamodar May 26, 2026
ca09149
fix(database): keep single-attribute indexes under MariaDB 768-byte l…
lohanidamodar May 26, 2026
aac7b95
feat(usage-query): add groupBy method for dimensional aggregation
lohanidamodar May 27, 2026
3d49192
feat(clickhouse): support groupBy dimension columns in aggregated find
lohanidamodar May 27, 2026
29d9292
feat(database-adapter): validate groupBy queries, share contract with…
lohanidamodar May 27, 2026
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
2 changes: 1 addition & 1 deletion .phpunit.result.cache
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":1,"defects":{"Utopia\\Tests\\Usage\\MetricTest::testGetEventIndexesReturnsIndexDefinitions":3,"Utopia\\Tests\\Usage\\MetricTest::testGetGaugeIndexesReturnsIndexDefinitions":3},"times":{"Utopia\\Tests\\Usage\\UsageQueryTest::testGroupByIntervalCreation":0.001,"Utopia\\Tests\\Usage\\UsageQueryTest::testGroupByIntervalAllValidIntervals":0,"Utopia\\Tests\\Usage\\UsageQueryTest::testGroupByIntervalInvalidInterval":0,"Utopia\\Tests\\Usage\\UsageQueryTest::testGroupByIntervalInvalidIntervalEmpty":0,"Utopia\\Tests\\Usage\\UsageQueryTest::testIsGroupByInterval":0,"Utopia\\Tests\\Usage\\UsageQueryTest::testExtractGroupByInterval":0,"Utopia\\Tests\\Usage\\UsageQueryTest::testExtractGroupByIntervalReturnsNullWhenMissing":0,"Utopia\\Tests\\Usage\\UsageQueryTest::testRemoveGroupByInterval":0,"Utopia\\Tests\\Usage\\UsageQueryTest::testValidIntervalsConstant":0,"Utopia\\Tests\\Usage\\UsageQueryTest::testUsageQueryExtendsQuery":0,"Utopia\\Tests\\Usage\\MetricTest::testGetEventSchemaReturnsAttributeDefinitions":0.001,"Utopia\\Tests\\Usage\\MetricTest::testGetGaugeSchemaReturnsAttributeDefinitions":0,"Utopia\\Tests\\Usage\\MetricTest::testGetSchemaReturnsEventSchema":0,"Utopia\\Tests\\Usage\\MetricTest::testGetEventIndexesReturnsIndexDefinitions":0,"Utopia\\Tests\\Usage\\MetricTest::testGetGaugeIndexesReturnsIndexDefinitions":0,"Utopia\\Tests\\Usage\\MetricTest::testGetIndexesReturnsEventIndexes":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateAcceptsValidEventData":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateAcceptsValidGaugeData":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateAcceptsMinimalData":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateRejectsMissingMetric":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateRejectsMissingValue":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateRejectsNonStringMetric":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateRejectsOversizedMetric":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateRejectsNonIntegerValue":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateAcceptsDateTimeForTime":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateAcceptsDatetimeStringForTime":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateRejectsInvalidDatetimeString":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateRejectsNonArrayTags":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateAcceptsEmptyTags":0,"Utopia\\Tests\\Usage\\MetricTest::testConstructorInitializesWithData":0,"Utopia\\Tests\\Usage\\MetricTest::testGetIdReturnsMetricId":0,"Utopia\\Tests\\Usage\\MetricTest::testGetIdReturnsEmptyStringWhenNotSet":0,"Utopia\\Tests\\Usage\\MetricTest::testGetMetricReturnsMetricName":0,"Utopia\\Tests\\Usage\\MetricTest::testGetValueReturnsValue":0,"Utopia\\Tests\\Usage\\MetricTest::testGetValueReturnsDefaultWhenNotSet":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTypeReturnsType":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTypeReturnsDefaultType":0,"Utopia\\Tests\\Usage\\MetricTest::testEventGettersReturnNullWhenNotSet":0,"Utopia\\Tests\\Usage\\MetricTest::testEventGettersReturnCorrectValues":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTimeReturnsTimestamp":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTimeReturnsNullWhenNotSet":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTagsReturnsTags":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTagsReturnsEmptyArrayWhenNotSet":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTenantReturnsTenantId":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTenantReturnsNullWhenNotSet":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTenantConvertsNumericToString":0,"Utopia\\Tests\\Usage\\MetricTest::testGetAttributesReturnsAllAttributes":0,"Utopia\\Tests\\Usage\\MetricTest::testGetAttributeReturnsValue":0,"Utopia\\Tests\\Usage\\MetricTest::testGetAttributeReturnsDefaultWhenNotSet":0,"Utopia\\Tests\\Usage\\MetricTest::testSetAttributeSetsAndReturnsSelf":0,"Utopia\\Tests\\Usage\\MetricTest::testSetAttributeSupportsChaining":0,"Utopia\\Tests\\Usage\\MetricTest::testHasAttributeReturnsTrueWhenExists":0,"Utopia\\Tests\\Usage\\MetricTest::testHasAttributeReturnsFalseWhenNotExists":0,"Utopia\\Tests\\Usage\\MetricTest::testRemoveAttributeRemovesAndReturnsSelf":0,"Utopia\\Tests\\Usage\\MetricTest::testIsEmptyReturnsFalseWhenIdSet":0,"Utopia\\Tests\\Usage\\MetricTest::testIsEmptyReturnsTrueWhenNoId":0,"Utopia\\Tests\\Usage\\MetricTest::testToArrayReturnsArray":0,"Utopia\\Tests\\Usage\\MetricTest::testEventColumnsConstant":0}}
{"version":1,"defects":{"Utopia\\Tests\\Usage\\MetricTest::testGetEventIndexesReturnsIndexDefinitions":3,"Utopia\\Tests\\Usage\\MetricTest::testGetGaugeIndexesReturnsIndexDefinitions":3,"Utopia\\Tests\\Usage\\MetricTest::testValidateRejectsNonArrayTags":3},"times":{"Utopia\\Tests\\Usage\\UsageQueryTest::testGroupByIntervalCreation":0.001,"Utopia\\Tests\\Usage\\UsageQueryTest::testGroupByIntervalAllValidIntervals":0,"Utopia\\Tests\\Usage\\UsageQueryTest::testGroupByIntervalInvalidInterval":0,"Utopia\\Tests\\Usage\\UsageQueryTest::testGroupByIntervalInvalidIntervalEmpty":0,"Utopia\\Tests\\Usage\\UsageQueryTest::testIsGroupByInterval":0,"Utopia\\Tests\\Usage\\UsageQueryTest::testExtractGroupByInterval":0,"Utopia\\Tests\\Usage\\UsageQueryTest::testExtractGroupByIntervalReturnsNullWhenMissing":0,"Utopia\\Tests\\Usage\\UsageQueryTest::testRemoveGroupByInterval":0,"Utopia\\Tests\\Usage\\UsageQueryTest::testValidIntervalsConstant":0,"Utopia\\Tests\\Usage\\UsageQueryTest::testUsageQueryExtendsQuery":0,"Utopia\\Tests\\Usage\\MetricTest::testGetEventSchemaReturnsAttributeDefinitions":0.001,"Utopia\\Tests\\Usage\\MetricTest::testGetGaugeSchemaReturnsAttributeDefinitions":0,"Utopia\\Tests\\Usage\\MetricTest::testGetSchemaReturnsEventSchema":0,"Utopia\\Tests\\Usage\\MetricTest::testGetEventIndexesReturnsIndexDefinitions":0,"Utopia\\Tests\\Usage\\MetricTest::testGetGaugeIndexesReturnsIndexDefinitions":0,"Utopia\\Tests\\Usage\\MetricTest::testGetIndexesReturnsEventIndexes":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateAcceptsValidEventData":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateAcceptsValidGaugeData":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateAcceptsMinimalData":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateRejectsMissingMetric":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateRejectsMissingValue":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateRejectsNonStringMetric":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateRejectsOversizedMetric":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateRejectsNonIntegerValue":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateAcceptsDateTimeForTime":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateAcceptsDatetimeStringForTime":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateRejectsInvalidDatetimeString":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateRejectsNonArrayTags":0,"Utopia\\Tests\\Usage\\MetricTest::testValidateAcceptsEmptyTags":0,"Utopia\\Tests\\Usage\\MetricTest::testConstructorInitializesWithData":0,"Utopia\\Tests\\Usage\\MetricTest::testGetIdReturnsMetricId":0,"Utopia\\Tests\\Usage\\MetricTest::testGetIdReturnsEmptyStringWhenNotSet":0,"Utopia\\Tests\\Usage\\MetricTest::testGetMetricReturnsMetricName":0,"Utopia\\Tests\\Usage\\MetricTest::testGetValueReturnsValue":0,"Utopia\\Tests\\Usage\\MetricTest::testGetValueReturnsDefaultWhenNotSet":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTypeReturnsType":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTypeReturnsDefaultType":0,"Utopia\\Tests\\Usage\\MetricTest::testEventGettersReturnNullWhenNotSet":0,"Utopia\\Tests\\Usage\\MetricTest::testEventGettersReturnCorrectValues":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTimeReturnsTimestamp":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTimeReturnsNullWhenNotSet":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTagsReturnsTags":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTagsReturnsEmptyArrayWhenNotSet":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTenantReturnsTenantId":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTenantReturnsNullWhenNotSet":0,"Utopia\\Tests\\Usage\\MetricTest::testGetTenantConvertsNumericToString":0,"Utopia\\Tests\\Usage\\MetricTest::testGetAttributesReturnsAllAttributes":0,"Utopia\\Tests\\Usage\\MetricTest::testGetAttributeReturnsValue":0,"Utopia\\Tests\\Usage\\MetricTest::testGetAttributeReturnsDefaultWhenNotSet":0,"Utopia\\Tests\\Usage\\MetricTest::testSetAttributeSetsAndReturnsSelf":0,"Utopia\\Tests\\Usage\\MetricTest::testSetAttributeSupportsChaining":0,"Utopia\\Tests\\Usage\\MetricTest::testHasAttributeReturnsTrueWhenExists":0,"Utopia\\Tests\\Usage\\MetricTest::testHasAttributeReturnsFalseWhenNotExists":0,"Utopia\\Tests\\Usage\\MetricTest::testRemoveAttributeRemovesAndReturnsSelf":0,"Utopia\\Tests\\Usage\\MetricTest::testIsEmptyReturnsFalseWhenIdSet":0,"Utopia\\Tests\\Usage\\MetricTest::testIsEmptyReturnsTrueWhenNoId":0,"Utopia\\Tests\\Usage\\MetricTest::testToArrayReturnsArray":0,"Utopia\\Tests\\Usage\\MetricTest::testEventColumnsConstant":0,"Utopia\\Tests\\Usage\\MetricTest::testGaugeColumnsConstant":0,"Utopia\\Tests\\Usage\\MetricTest::testEventSchemaHasAllNewColumns":0,"Utopia\\Tests\\Usage\\MetricTest::testGaugeSchemaHasTeamAndResourceColumns":0}}
72 changes: 56 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Although this library is part of the [Utopia Framework](https://github.com/utopi
## Features

- **Two Table Architecture**: Separate events and gauges tables optimized for their access patterns
- **Events Table**: Request-level metrics with dedicated columns (path, method, status, resource, country, userAgent)
- **Events Table**: Request-level metrics with dedicated columns for every dimension we filter on (path/method/status, service/resource ids, team ids, country/region, hostname, parsed UA fields)
- **Gauges Table**: Simple resource snapshots (storage size, user count, etc.)
- **Query-Time Aggregation**: No write-time period fan-out — aggregate by any interval at query time
- **Daily Materialized View**: Pre-aggregated daily SummingMergeTree for fast billing queries
Expand Down Expand Up @@ -72,34 +72,56 @@ $usage->setup();

Events are request-level metrics like bandwidth, executions, API calls. They are summed when aggregated.

Event-specific columns: `path`, `method`, `status`, `resource`, `resourceId`, `country`, `userAgent`
Event-specific columns (see `Metric::EVENT_COLUMNS`): `path`, `method`, `status`,
`service`, `resource`, `resourceId`, `resourceInternalId`, `teamId`,
`teamInternalId`, `country`, `region`, `hostname`, `osCode`, `osName`,
`osVersion`, `clientType`, `clientCode`, `clientName`, `clientVersion`,
`clientEngine`, `clientEngineVersion`, `deviceName`, `deviceBrand`,
`deviceModel`.

```php
// Collect events — values accumulate in-memory buffer (summed per metric)
$usage->collect('bandwidth', 5000, Usage::TYPE_EVENT, [
'path' => '/v1/storage/files',
'method' => 'POST',
'status' => '201',
'service' => 'storage',
'resource' => 'bucket',
'resourceId' => 'abc123',
'resourceInternalId' => '42',
'teamId' => 'team_x',
'teamInternalId' => '7',
'country' => 'US',
'userAgent' => 'AppwriteSDK/1.0',
'region' => 'us-east',
'hostname' => 'app.example.com',
'osName' => 'iOS',
'clientName' => 'Appwrite SDK',
'deviceName' => 'smartphone',
]);

// Event columns are auto-extracted from tags into dedicated columns
// Remaining tags stay in the JSON tags column
```

### Strict tag keys

All keys passed in the `tags` array must map to a known event or gauge
column (see `Metric::EVENT_COLUMNS` and `Metric::GAUGE_COLUMNS`). Unknown
keys throw at write time — there is no JSON catch-all. To add a new
dimension, widen the schema and bump the library version.

### Gauges (Point-in-Time)

Gauges are resource snapshots like storage size, user count, file count. Last-write-wins semantics.

Gauge-specific columns (see `Metric::GAUGE_COLUMNS`): `teamId`,
`teamInternalId`, `resourceId`, `resourceInternalId`.

```php
// Collect gauges — last value wins per metric in buffer
$usage->collect('users', 1500, Usage::TYPE_GAUGE);
$usage->collect('storage.size', 1048576, Usage::TYPE_GAUGE, [
'resource' => 'bucket',
'teamId' => 'team_x',
'teamInternalId' => '7',
'resourceId' => 'abc123',
'resourceInternalId' => '42',
]);
```

Expand All @@ -121,12 +143,12 @@ $usage->setFlushInterval(10); // Flush after 10 seconds (default: 20)
```php
// Write directly without buffering
$usage->addBatch([
['metric' => 'requests', 'value' => 100, 'tags' => ['path' => '/v1/users']],
['metric' => 'bandwidth', 'value' => 50000, 'tags' => ['country' => 'DE']],
['metric' => 'requests', 'value' => 100, 'tags' => ['path' => '/v1/users', 'method' => 'GET']],
['metric' => 'bandwidth', 'value' => 50000, 'tags' => ['country' => 'DE', 'region' => 'fra']],
], Usage::TYPE_EVENT);

$usage->addBatch([
['metric' => 'users', 'value' => 42, 'tags' => []],
['metric' => 'users', 'value' => 42, 'tags' => ['teamId' => 'team_x']],
], Usage::TYPE_GAUGE);
```

Expand Down Expand Up @@ -250,11 +272,21 @@ $usage->purge([], Usage::TYPE_GAUGE);
| path | Nullable(String) | API endpoint path |
| method | Nullable(String) | HTTP method |
| status | Nullable(String) | HTTP status code |
| resource | Nullable(String) | Resource type |
| resourceId | Nullable(String) | Resource ID |
| country | LowCardinality(Nullable(String)) | ISO country code |
| userAgent | Nullable(String) | User agent string |
| tags | Nullable(String) | JSON for extra metadata |
| service | LowCardinality(Nullable(String)) | API service (storage, databases, …) |
| resource | LowCardinality(Nullable(String)) | Resource type (bucket, file, …) |
| resourceId | Nullable(String) | External resource id |
| resourceInternalId | Nullable(String) | Internal resource sequence |
| teamId | Nullable(String) | External team id |
| teamInternalId | Nullable(String) | Internal team sequence |
| country | LowCardinality(Nullable(String)) | ISO country code (lowercased) |
| region | LowCardinality(Nullable(String)) | Region code (lowercased) |
| hostname | Nullable(String) | Caller origin host |
| osCode, osName | LowCardinality(Nullable(String)) | Parsed OS short code / name |
| osVersion | Nullable(String) | Parsed OS version |
| clientType, clientCode, clientName, clientEngine | LowCardinality(Nullable(String)) | Parsed client identity |
| clientVersion, clientEngineVersion | Nullable(String) | Parsed client versions |
| deviceName, deviceBrand | LowCardinality(Nullable(String)) | Parsed device identity |
| deviceModel | Nullable(String) | Parsed device model |
| tenant | Nullable(String) | Tenant ID (shared tables) |

### Gauges Table Schema
Expand All @@ -265,7 +297,10 @@ $usage->purge([], Usage::TYPE_GAUGE);
| metric | String | Metric name |
| value | Int64 | Current value |
| time | DateTime64(3) | Snapshot timestamp |
| tags | Nullable(String) | JSON metadata |
| teamId | Nullable(String) | External team id |
| teamInternalId | Nullable(String) | Internal team sequence |
| resourceId | Nullable(String) | External resource id |
| resourceInternalId | Nullable(String) | Internal resource sequence |
| tenant | Nullable(String) | Tenant ID (shared tables) |

### Daily Table Schema
Expand All @@ -275,6 +310,11 @@ $usage->purge([], Usage::TYPE_GAUGE);
| metric | String | Metric name |
| value | Int64 | Aggregated daily sum |
| time | DateTime64(3) | Day start timestamp |
| resource | LowCardinality(Nullable(String)) | Resource type |
| resourceId | Nullable(String) | External resource id |
| resourceInternalId | Nullable(String) | Internal resource sequence |
| teamId | Nullable(String) | External team id |
| teamInternalId | Nullable(String) | Internal team sequence |
| tenant | Nullable(String) | Tenant ID (shared tables) |

### Creating Custom Adapters
Expand Down
Loading
Loading