Skip to content

Commit 3111240

Browse files
Merge pull request #5 from utopia-php/feat/CLO-4383-usage-refactor
feat: refactor usage schema for richer event dimensions
2 parents 9bff4b7 + 29d9292 commit 3111240

11 files changed

Lines changed: 1260 additions & 400 deletions

File tree

.phpunit.result.cache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +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}}
1+
{"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}}

README.md

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Although this library is part of the [Utopia Framework](https://github.com/utopi
1010
## Features
1111

1212
- **Two Table Architecture**: Separate events and gauges tables optimized for their access patterns
13-
- **Events Table**: Request-level metrics with dedicated columns (path, method, status, resource, country, userAgent)
13+
- **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)
1414
- **Gauges Table**: Simple resource snapshots (storage size, user count, etc.)
1515
- **Query-Time Aggregation**: No write-time period fan-out — aggregate by any interval at query time
1616
- **Daily Materialized View**: Pre-aggregated daily SummingMergeTree for fast billing queries
@@ -72,34 +72,56 @@ $usage->setup();
7272

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

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

7782
```php
7883
// Collect events — values accumulate in-memory buffer (summed per metric)
7984
$usage->collect('bandwidth', 5000, Usage::TYPE_EVENT, [
8085
'path' => '/v1/storage/files',
8186
'method' => 'POST',
8287
'status' => '201',
88+
'service' => 'storage',
8389
'resource' => 'bucket',
8490
'resourceId' => 'abc123',
91+
'resourceInternalId' => '42',
92+
'teamId' => 'team_x',
93+
'teamInternalId' => '7',
8594
'country' => 'US',
86-
'userAgent' => 'AppwriteSDK/1.0',
95+
'region' => 'us-east',
96+
'hostname' => 'app.example.com',
97+
'osName' => 'iOS',
98+
'clientName' => 'Appwrite SDK',
99+
'deviceName' => 'smartphone',
87100
]);
88-
89-
// Event columns are auto-extracted from tags into dedicated columns
90-
// Remaining tags stay in the JSON tags column
91101
```
92102

103+
### Strict tag keys
104+
105+
All keys passed in the `tags` array must map to a known event or gauge
106+
column (see `Metric::EVENT_COLUMNS` and `Metric::GAUGE_COLUMNS`). Unknown
107+
keys throw at write time — there is no JSON catch-all. To add a new
108+
dimension, widen the schema and bump the library version.
109+
93110
### Gauges (Point-in-Time)
94111

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

114+
Gauge-specific columns (see `Metric::GAUGE_COLUMNS`): `teamId`,
115+
`teamInternalId`, `resourceId`, `resourceInternalId`.
116+
97117
```php
98118
// Collect gauges — last value wins per metric in buffer
99119
$usage->collect('users', 1500, Usage::TYPE_GAUGE);
100120
$usage->collect('storage.size', 1048576, Usage::TYPE_GAUGE, [
101-
'resource' => 'bucket',
121+
'teamId' => 'team_x',
122+
'teamInternalId' => '7',
102123
'resourceId' => 'abc123',
124+
'resourceInternalId' => '42',
103125
]);
104126
```
105127

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

128150
$usage->addBatch([
129-
['metric' => 'users', 'value' => 42, 'tags' => []],
151+
['metric' => 'users', 'value' => 42, 'tags' => ['teamId' => 'team_x']],
130152
], Usage::TYPE_GAUGE);
131153
```
132154

@@ -250,11 +272,21 @@ $usage->purge([], Usage::TYPE_GAUGE);
250272
| path | Nullable(String) | API endpoint path |
251273
| method | Nullable(String) | HTTP method |
252274
| status | Nullable(String) | HTTP status code |
253-
| resource | Nullable(String) | Resource type |
254-
| resourceId | Nullable(String) | Resource ID |
255-
| country | LowCardinality(Nullable(String)) | ISO country code |
256-
| userAgent | Nullable(String) | User agent string |
257-
| tags | Nullable(String) | JSON for extra metadata |
275+
| service | LowCardinality(Nullable(String)) | API service (storage, databases, …) |
276+
| resource | LowCardinality(Nullable(String)) | Resource type (bucket, file, …) |
277+
| resourceId | Nullable(String) | External resource id |
278+
| resourceInternalId | Nullable(String) | Internal resource sequence |
279+
| teamId | Nullable(String) | External team id |
280+
| teamInternalId | Nullable(String) | Internal team sequence |
281+
| country | LowCardinality(Nullable(String)) | ISO country code (lowercased) |
282+
| region | LowCardinality(Nullable(String)) | Region code (lowercased) |
283+
| hostname | Nullable(String) | Caller origin host |
284+
| osCode, osName | LowCardinality(Nullable(String)) | Parsed OS short code / name |
285+
| osVersion | Nullable(String) | Parsed OS version |
286+
| clientType, clientCode, clientName, clientEngine | LowCardinality(Nullable(String)) | Parsed client identity |
287+
| clientVersion, clientEngineVersion | Nullable(String) | Parsed client versions |
288+
| deviceName, deviceBrand | LowCardinality(Nullable(String)) | Parsed device identity |
289+
| deviceModel | Nullable(String) | Parsed device model |
258290
| tenant | Nullable(String) | Tenant ID (shared tables) |
259291

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

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

280320
### Creating Custom Adapters

0 commit comments

Comments
 (0)