Repository navigation
fix(common-utils): keep metric series from different scopes apart - #3598
BilalAtique wants to merge 4 commits into
Conversation
Two instrumentation scopes can report the same metric with the same resource, scope and point attributes. The OTel data model makes them two streams, but the metric CTEs keyed a series on cityHash64(ScopeAttributes, ResourceAttributes, Attributes) only, so they merged the two scopes into one series. The gauge Bucketed CTE kept one scope's value per bucket, so sum and count read one scope and a group-by on ScopeName showed one group. The sum, histogram and exponential histogram windows compared one scope's cumulative total with the other's, so increase and count read the idle scope's whole total as new on every point. The gauge, sum, histogram and exponential histogram queries now put ScopeName in the series key. Closes ClickHouse#1640
|
@BilalAtique is attempting to deploy a commit to the HyperDX Team on Vercel. A member of the Team first needs to authorize it. |
A NULL argument makes cityHash64 return NULL. With a Nullable(String) ScopeName, every series whose ScopeName is NULL got the same key and merged, also when their attributes differ. HyperDX also does not require ScopeName on metric tables, so a custom histogram table without it, which the histogram queries read before, failed with an unknown identifier. New renderMetricSeriesKey looks up ScopeName in the DESCRIBE result that the query already caches. It hashes a Nullable ScopeName as '', and keeps the key without ScopeName when the table has no ScopeName column or the lookup fails. The gauge, sum and histogram paths use it.
PR Review✅ No issues found. |
Deep Review✅ No critical issues found. This is a focused, well-tested correctness fix: 🟡 P2 — recommended
🔵 P3 nitpicks (2)
Pre-existing (not introduced by this diff)
Reviewers (11): correctness, adversarial, security, performance, reliability, kieran-typescript, testing, maintainability, project-standards, agent-native, learnings-researcher. Testing gaps:
No finding blocks merge automatically. A maintainer will expect P0/P1 findings in code this PR changes to be fixed; P2/P3 are your call -- fix or reply. Never fix findings about surrounding code here; reply instead. Do not widen the PR. How to respond |
|
Re the Deep Review above (130256e):
|
|
Re the PR Review above: fixed in fe74ddc. The That is two rounds of bot findings, so I'll stop here. Could a maintainer approve the workflows and look at the scope when you have time? |
Summary
Two instrumentation scopes can report the same metric with the same resource, scope and point attributes. For example, two instrumentation libraries in one process can both record one HTTP metric. The OTel data model makes these two streams, because the instrumentation scope is part of a stream's identity. The metric queries in
renderChartConfigkeyed a series oncityHash64(ScopeAttributes, ResourceAttributes, Attributes)only, so they read the two scopes as one series. Alerts use the same queries.Effects on
main, measured in the new integration test. Both scopes report at the sameTimeUnix. Scopeahas an idle series, and scopebgets 5 new observations a minute:aat 10, scopebat 20):sumis 20 instead of 30, andcountis 1 instead of 2. A group-by onScopeNameshows one group,scope.a, with scopeb's value of 20.aidle at 1000):increaseis 1000 a minute instead of 5. The window compares one scope's total with the other's, so the idle scope's whole total counts as new.countis 1005 a minute instead of 5, and p50 is 5.02 although all new observations are in (10, 30].countis 1005 a minute instead of 5, and p50 is 2.09 although all new observations are in (4, 4.36].The
mainvalues were the same in 3 runs. Rows with the sameTimeUnixhave no defined order in the window, so other data can give other wrong values.Changes:
renderMetricSeriesKeyinrenderChartConfig.tsreturns the series keycityHash64(ScopeName, ScopeAttributes, ResourceAttributes, Attributes). The gauge and sumSourceCTEs use it forAttributesHash. The histogram and exponential histogram queries (count and quantile) get it through a newattrHashExprinput, which replaces theATTR_HASH_EXPRconstant inhistogram.ts. Single and multi-series metric charts and metric alerts all get it.ScopeNameis in the key only when the table has aScopeNamecolumn. HyperDX does not require it, and the histogram queries did not read it, so a custom histogram table without it gets the same query as before. A failed column lookup also keeps the old key. The lookup reads theDESCRIBEresult that the same query already caches, so it adds no ClickHouse query.NullableScopeNameis hashed asifNull(ScopeName, '').cityHash64returnsNULLwhen an argument isNULL, so otherwise every series with aNULLScopeNamewould get the same key, whatever its attributes.Things to be aware of:
ScopeNamecolumn).ScopeName, the histogram queries now read one more column (ScopeName, aStringin the OTel schema). The gauge and sum queries select*, so they read it already.ScopeVersionandScopeSchemaUrlare not in the key. The OTel data model also counts the scope version, but Hash by ScopeName to prevent double counts #1640 is about the name only, so I kept the change to that. I can add the version if you want it.SeriesHash), which no chart query reads.Tests
packages/common-utils/src/__tests__/metricScopeName.int.test.ts(ClickHouse): one table per metric type with the OTel columns, and two scopes that differ only inScopeNameand report at the sameTimeUnix, as one SDK reader does. 12 tests: gaugesumandcountover both scopes, a gauge group-by onScopeName, sumincrease, a counter in one scope only (a control), histogramcountand p50, and exponential histogramcountand p50. Then two custom tables: a gauge with aNullable(String)ScopeName, where two series with aNULLscope and other attributes stay apart (sumandcount), and a histogram table withoutScopeName.renderChartConfig.test.ts(14 tests): the key hasScopeNamefor each metric type and aggFn when the table has the column. It does not when a histogram table has noScopeNameor the lookup fails.LowCardinality(String)gives the same key, andNullable(String)andLowCardinality(Nullable(String))give theifNullkey.With the code from
main, 8 of the 12 integration tests fail (the control and the 3 custom-table tests pass), and 9 of the 14 unit tests fail (the 5 "same query as before" cases pass on both). Without the column lookup, the 3 custom-table tests fail:NULL-scope series merge (sum20 instead of 30,count1 instead of 2), and the histogram query fails withUnknown expression or function identifier ScopeName.renderChartConfig.test.tsandbuilderToRawSql.test.ts(305 tests, no snapshot changes) pass, and so doqueryChartConfig.int.test.tsandmetricNoRecordedValue.int.test.ts(91 tests, no changes). ESLint (no new warnings) and Prettier pass. I ran the integration tests on ClickHouse 26.8.36.3, the version CI uses.I did not run the
apiintegration tests (alerts, MCP),tscfor the whole package, or the UI steps below. I ran the SQL below against the OTel gauge schema on ClickHouse 26.8.36.3: the old key gives 1 series for its 12 rows, and the new key gives 2.How to reproduce
Starting point:
yarn dev, which creates the OTel metric tables in thedefaultdatabase of the local ClickHouse.On
main:Insert one gauge from two scopes for the last 6 minutes. Only
ScopeNameis different:Open Chart Explorer. Pick the metrics source and the gauge
repro.cpuwith the Sum aggregation, over the last 15 minutes with a 1 minute granularity.Each minute shows 10 or 20 (one scope), not 30.
Group by
ScopeName. Only one line shows.On this branch:
scope.aat 10 andscope.bat 20.The new integration test covers the other metric types:
make dev-int-common-utils FILE=metricScopeName.How to test on Vercel preview
N/A — non-UI change.
References