mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-19 03:10:40 +01:00
Compare commits
2 Commits
main
...
feat/manua
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dec9b37f5 | ||
|
|
8bb3512768 |
@@ -56,10 +56,10 @@ const config: Config.InitialOptions = {
|
||||
transformIgnorePatterns: [
|
||||
// @chenglou/pretext is ESM-only; @signozhq/ui pulls it in via text-ellipsis.
|
||||
// Pattern 1: allow .pnpm virtual store through (handled by pattern 2), plus root-level ESM packages.
|
||||
'node_modules/(?!(\\.pnpm|react-json-tree|react-base16-styling|lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou/pretext|@signozhq/design-tokens|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard|react-markdown|vfile|vfile-message|unist-util-stringify-position|unified|bail|is-plain-obj|trough|remark-parse|mdast-util-from-markdown|mdast-util-to-string|micromark|micromark-core-commonmark|micromark-extension-gfm|micromark-extension-gfm-autolink-literal|micromark-extension-gfm-footnote|micromark-extension-gfm-strikethrough|micromark-extension-gfm-table|micromark-extension-gfm-tagfilter|micromark-extension-gfm-task-list-item|micromark-factory-destination|micromark-factory-label|micromark-factory-space|micromark-factory-title|micromark-factory-whitespace|micromark-util-character|micromark-util-chunked|micromark-util-classify-character|micromark-util-combine-extensions|micromark-util-decode-numeric-character-reference|micromark-util-decode-string|micromark-util-encode|micromark-util-html-tag-name|micromark-util-normalize-identifier|micromark-util-resolve-all|micromark-util-sanitize-uri|micromark-util-subtokenize|micromark-util-symbol|micromark-util-types|decode-named-character-reference|remark-rehype|mdast-util-to-hast|unist-util-position|trim-lines|unist-util-visit|unist-util-visit-parents|unist-util-is|unist-util-generated|mdast-util-definitions|property-information|hast-util-whitespace|space-separated-tokens|comma-separated-tokens|rehype-raw|hast-util-raw|hast-util-from-parse5|devlop|hastscript|hast-util-parse-selector|vfile-location|web-namespaces|hast-util-to-parse5|zwitch|html-void-elements)/)',
|
||||
'node_modules/(?!(\\.pnpm|lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou/pretext|@signozhq/design-tokens|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard|react-markdown|vfile|vfile-message|unist-util-stringify-position|unified|bail|is-plain-obj|trough|remark-parse|mdast-util-from-markdown|mdast-util-to-string|micromark|micromark-core-commonmark|micromark-extension-gfm|micromark-extension-gfm-autolink-literal|micromark-extension-gfm-footnote|micromark-extension-gfm-strikethrough|micromark-extension-gfm-table|micromark-extension-gfm-tagfilter|micromark-extension-gfm-task-list-item|micromark-factory-destination|micromark-factory-label|micromark-factory-space|micromark-factory-title|micromark-factory-whitespace|micromark-util-character|micromark-util-chunked|micromark-util-classify-character|micromark-util-combine-extensions|micromark-util-decode-numeric-character-reference|micromark-util-decode-string|micromark-util-encode|micromark-util-html-tag-name|micromark-util-normalize-identifier|micromark-util-resolve-all|micromark-util-sanitize-uri|micromark-util-subtokenize|micromark-util-symbol|micromark-util-types|decode-named-character-reference|remark-rehype|mdast-util-to-hast|unist-util-position|trim-lines|unist-util-visit|unist-util-visit-parents|unist-util-is|unist-util-generated|mdast-util-definitions|property-information|hast-util-whitespace|space-separated-tokens|comma-separated-tokens|rehype-raw|hast-util-raw|hast-util-from-parse5|devlop|hastscript|hast-util-parse-selector|vfile-location|web-namespaces|hast-util-to-parse5|zwitch|html-void-elements)/)',
|
||||
// Pattern 2: pnpm virtual store — ignore everything except ESM-only packages.
|
||||
// pnpm encodes scoped packages as @scope+name@version, so match on scope prefix.
|
||||
'node_modules/\\.pnpm/(?!(react-json-tree|react-base16-styling|lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard|react-markdown|vfile|vfile-message|unist-util-stringify-position|unified|bail|is-plain-obj|trough|remark-parse|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|remark-rehype|mdast-util-to-hast|unist-util-position|trim-lines|unist-util-visit|unist-util-visit-parents|unist-util-is|unist-util-generated|mdast-util-definitions|property-information|hast-util-whitespace|space-separated-tokens|comma-separated-tokens|rehype-raw|hast-util-raw|hast-util-from-parse5|devlop|hastscript|hast-util-parse-selector|vfile-location|web-namespaces|hast-util-to-parse5|zwitch|html-void-elements)[^/]*/node_modules)',
|
||||
'node_modules/\\.pnpm/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard|react-markdown|vfile|vfile-message|unist-util-stringify-position|unified|bail|is-plain-obj|trough|remark-parse|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|remark-rehype|mdast-util-to-hast|unist-util-position|trim-lines|unist-util-visit|unist-util-visit-parents|unist-util-is|unist-util-generated|mdast-util-definitions|property-information|hast-util-whitespace|space-separated-tokens|comma-separated-tokens|rehype-raw|hast-util-raw|hast-util-from-parse5|devlop|hastscript|hast-util-parse-selector|vfile-location|web-namespaces|hast-util-to-parse5|zwitch|html-void-elements)[^/]*/node_modules)',
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
||||
|
||||
@@ -197,7 +197,7 @@
|
||||
"oxfmt": "0.54.0",
|
||||
"oxlint": "1.69.0",
|
||||
"oxlint-tsgolint": "0.23.0",
|
||||
"postcss": "8.5.26",
|
||||
"postcss": "8.5.14",
|
||||
"postcss-scss": "4.0.9",
|
||||
"react-resizable": "3.0.4",
|
||||
"redux-mock-store": "1.5.4",
|
||||
|
||||
316
frontend/pnpm-lock.yaml
generated
316
frontend/pnpm-lock.yaml
generated
@@ -6,19 +6,12 @@ settings:
|
||||
|
||||
overrides:
|
||||
'@babel/core@<=7.29.0': '>=7.29.6 <8'
|
||||
'@istanbuljs/load-nyc-config>js-yaml': '>=4.3.1 <5'
|
||||
brace-expansion@<1.1.18: '>=1.1.18 <2'
|
||||
brace-expansion@>=2.0.0 <2.1.4: '>=2.1.4 <3'
|
||||
brace-expansion@>=5.0.0 <5.0.9: '>=5.0.9 <6'
|
||||
'@istanbuljs/load-nyc-config>js-yaml': '>=4.2.0 <5'
|
||||
cookie@<0.7.0: '>=0.7.1 <1'
|
||||
dompurify@<=3.4.10: '>=3.4.11 <4'
|
||||
esbuild@>=0.27.3 <0.28.1: '>=0.28.1 <0.29.0'
|
||||
fast-uri@<3.1.5: '>=3.1.5 <4'
|
||||
immutable@<5.1.8: '>=5.1.8 <6'
|
||||
js-cookie@<=3.0.5: '>=3.0.7 <4'
|
||||
js-yaml@>=4.0.0 <4.3.1: '>=4.3.1 <5'
|
||||
less@<4.5.0: '>=4.5.0 <5'
|
||||
nanoid@<3.3.18: '>=3.3.18 <4'
|
||||
js-yaml@>=4.0.0 <=4.1.1: '>=4.2.0 <5'
|
||||
prismjs@<1.30.0: '>=1.30.0 <2'
|
||||
react-router@>=6.7.0 <6.30.4: '>=6.30.4 <7'
|
||||
tmp@<0.2.6: '>=0.2.6 <0.3.0'
|
||||
@@ -105,7 +98,7 @@ importers:
|
||||
version: 3.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@vitejs/plugin-react':
|
||||
specifier: 5.1.4
|
||||
version: 5.1.4(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))
|
||||
version: 5.1.4(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))
|
||||
ansi-to-html:
|
||||
specifier: 0.7.2
|
||||
version: 0.7.2
|
||||
@@ -318,10 +311,10 @@ importers:
|
||||
version: 14.0.1
|
||||
vite:
|
||||
specifier: npm:rolldown-vite@7.3.1
|
||||
version: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
version: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
vite-plugin-html:
|
||||
specifier: 3.2.2
|
||||
version: 3.2.2(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))
|
||||
version: 3.2.2(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))
|
||||
zod:
|
||||
specifier: 4.3.6
|
||||
version: 4.3.6
|
||||
@@ -474,11 +467,11 @@ importers:
|
||||
specifier: 0.23.0
|
||||
version: 0.23.0
|
||||
postcss:
|
||||
specifier: 8.5.26
|
||||
version: 8.5.26
|
||||
specifier: 8.5.14
|
||||
version: 8.5.14
|
||||
postcss-scss:
|
||||
specifier: 4.0.9
|
||||
version: 4.0.9(postcss@8.5.26)
|
||||
version: 4.0.9(postcss@8.5.14)
|
||||
react-resizable:
|
||||
specifier: 3.0.4
|
||||
version: 3.0.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -508,16 +501,16 @@ importers:
|
||||
version: 1.6.0(react@18.2.0)
|
||||
vite-plugin-checker:
|
||||
specifier: 0.12.0
|
||||
version: 0.12.0(eslint@10.2.1(jiti@2.6.1))(meow@13.2.0)(optionator@0.9.4)(oxlint@1.69.0(oxlint-tsgolint@0.23.0))(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(stylelint@17.7.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
version: 0.12.0(eslint@10.2.1(jiti@2.6.1))(meow@13.2.0)(optionator@0.9.4)(oxlint@1.69.0(oxlint-tsgolint@0.23.0))(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(stylelint@17.7.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
vite-plugin-compression:
|
||||
specifier: 0.5.1
|
||||
version: 0.5.1(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))
|
||||
version: 0.5.1(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))
|
||||
vite-plugin-image-optimizer:
|
||||
specifier: 2.0.3
|
||||
version: 2.0.3(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(sharp@0.35.0)(svgo@4.0.2)
|
||||
version: 2.0.3(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(sharp@0.35.0)(svgo@4.0.2)
|
||||
vite-tsconfig-paths:
|
||||
specifier: 6.1.1
|
||||
version: 6.1.1(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(typescript@5.9.3)
|
||||
version: 6.1.1(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(typescript@5.9.3)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -4166,15 +4159,15 @@ packages:
|
||||
boolbase@1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
|
||||
brace-expansion@1.1.18:
|
||||
resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==}
|
||||
brace-expansion@1.1.15:
|
||||
resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==}
|
||||
|
||||
brace-expansion@2.1.4:
|
||||
resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==}
|
||||
brace-expansion@2.1.1:
|
||||
resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==}
|
||||
|
||||
brace-expansion@5.0.9:
|
||||
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
|
||||
engines: {node: 20 || >=22}
|
||||
brace-expansion@5.0.7:
|
||||
resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
braces@3.0.3:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
@@ -4466,9 +4459,8 @@ packages:
|
||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
copy-anything@3.0.5:
|
||||
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
|
||||
engines: {node: '>=12.13'}
|
||||
copy-anything@2.0.6:
|
||||
resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==}
|
||||
|
||||
copy-text-to-clipboard@3.2.2:
|
||||
resolution: {integrity: sha512-T6SqyLd1iLuqPA90J5N4cTalrtovCySh58iiZDGJ6FGznbclKh4UI+FGacQSgFzwKG77W7XT5gwbVEbd9cIH1A==}
|
||||
@@ -4715,14 +4707,6 @@ packages:
|
||||
debounce@1.2.1:
|
||||
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
|
||||
|
||||
debug@2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
debug@3.2.7:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
peerDependencies:
|
||||
@@ -5116,8 +5100,8 @@ packages:
|
||||
fast-shallow-equal@1.0.0:
|
||||
resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==}
|
||||
|
||||
fast-uri@3.1.5:
|
||||
resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==}
|
||||
fast-uri@3.1.2:
|
||||
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
|
||||
|
||||
fast_array_intersect@1.1.0:
|
||||
resolution: {integrity: sha512-/DCilZlUdz2XyNDF+ASs0PwY+RKG9Y4Silp/gbS72Cvbg4oibc778xcecg+pnNyiNHYgh/TApsiDTjpdniyShw==}
|
||||
@@ -5572,14 +5556,19 @@ packages:
|
||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
image-size@0.5.5:
|
||||
resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
hasBin: true
|
||||
|
||||
immediate@3.0.6:
|
||||
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||
|
||||
immer@11.1.3:
|
||||
resolution: {integrity: sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==}
|
||||
|
||||
immutable@5.1.9:
|
||||
resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==}
|
||||
immutable@5.1.5:
|
||||
resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==}
|
||||
|
||||
import-fresh@3.3.1:
|
||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||
@@ -5822,9 +5811,8 @@ packages:
|
||||
resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-what@4.1.16:
|
||||
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
|
||||
engines: {node: '>=12.13'}
|
||||
is-what@3.14.1:
|
||||
resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==}
|
||||
|
||||
is-wsl@3.1.1:
|
||||
resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==}
|
||||
@@ -6101,8 +6089,8 @@ packages:
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
js-yaml@4.3.1:
|
||||
resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==}
|
||||
js-yaml@4.3.0:
|
||||
resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
|
||||
hasBin: true
|
||||
|
||||
jsdom@20.0.3:
|
||||
@@ -6185,9 +6173,9 @@ packages:
|
||||
lerc@3.0.0:
|
||||
resolution: {integrity: sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==}
|
||||
|
||||
less@4.9.0:
|
||||
resolution: {integrity: sha512-umRhrCH7fCi8Uj2RcwKjJdvUORTjeWqkdKx0LbcZvjIwsAVsnIAGcxHaqowPeBFBjQuWOeC/bve0AlpFzF/+SQ==}
|
||||
engines: {node: '>=18'}
|
||||
less@4.4.0:
|
||||
resolution: {integrity: sha512-kdTwsyRuncDfjEs0DlRILWNvxhDG/Zij4YLO4TMJgDLW+8OzpfkdPnRgrsRuY1o+oaxJGWsps5f/RVBgGmmN0w==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
leven@3.1.0:
|
||||
@@ -6376,14 +6364,14 @@ packages:
|
||||
resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
make-dir@2.1.0:
|
||||
resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
make-dir@3.1.0:
|
||||
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
make-dir@5.1.0:
|
||||
resolution: {integrity: sha512-IfpFq6UM39dUNiphpA6uDezNx/AvWyhwfICWPR3t1VspkgkMZrL+Rk1RbN1bx+aeNYwOrqGJgEgV3yotk+ZUVw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
make-error@1.3.6:
|
||||
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||
|
||||
@@ -6703,9 +6691,6 @@ packages:
|
||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
ms@2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
|
||||
ms@2.1.2:
|
||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||
|
||||
@@ -6734,8 +6719,8 @@ packages:
|
||||
nano-time@1.0.0:
|
||||
resolution: {integrity: sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==}
|
||||
|
||||
nanoid@3.3.18:
|
||||
resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
|
||||
nanoid@3.3.11:
|
||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
@@ -6747,11 +6732,6 @@ packages:
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
needle@2.9.1:
|
||||
resolution: {integrity: sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==}
|
||||
engines: {node: '>= 4.4.x'}
|
||||
hasBin: true
|
||||
|
||||
needle@3.2.0:
|
||||
resolution: {integrity: sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==}
|
||||
engines: {node: '>= 4.4.x'}
|
||||
@@ -7086,6 +7066,10 @@ packages:
|
||||
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
pify@4.0.1:
|
||||
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
pirates@4.0.7:
|
||||
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -7147,8 +7131,8 @@ packages:
|
||||
postcss-value-parser@4.2.0:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
||||
postcss@8.5.26:
|
||||
resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
|
||||
postcss@8.5.14:
|
||||
resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
posthog-js@1.298.0:
|
||||
@@ -7194,9 +7178,6 @@ packages:
|
||||
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
probe-image-size@7.4.0:
|
||||
resolution: {integrity: sha512-cdEprVtZxV+awMde9X+4jILBFYh4CARxVrQaMl4wY4YcPWbul9jntXrIW95NInBDyJwcVUP3U0T6yukN8rMBaQ==}
|
||||
|
||||
process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
@@ -7943,7 +7924,7 @@ packages:
|
||||
'@types/node': ^20.19.0 || >=22.12.0
|
||||
esbuild: '>=0.28.1 <0.29.0'
|
||||
jiti: '>=1.21.0'
|
||||
less: '>=4.5.0 <5'
|
||||
less: ^4.0.0
|
||||
sass: ^1.70.0
|
||||
sass-embedded: ^1.70.0
|
||||
stylus: '>=0.54.8'
|
||||
@@ -8063,6 +8044,10 @@ packages:
|
||||
resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==}
|
||||
engines: {node: ^14.0.0 || >=16.0.0}
|
||||
|
||||
semver@5.7.2:
|
||||
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
||||
hasBin: true
|
||||
|
||||
semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
@@ -8220,9 +8205,6 @@ packages:
|
||||
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
stream-parser@0.3.1:
|
||||
resolution: {integrity: sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==}
|
||||
|
||||
strict-event-emitter@0.2.8:
|
||||
resolution: {integrity: sha512-KDf/ujU8Zud3YaLtMCcTI4xkZlZVIYxTLr+XIULexP+77EEVWixeXroLUXQXiVtH4XH2W7jr/3PT1v3zBuvc3A==}
|
||||
|
||||
@@ -9087,7 +9069,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jsdevtools/ono': 7.1.3
|
||||
'@types/json-schema': 7.0.15
|
||||
js-yaml: 4.3.1
|
||||
js-yaml: 4.3.0
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
dependencies:
|
||||
@@ -10587,7 +10569,7 @@ snapshots:
|
||||
camelcase: 5.3.1
|
||||
find-up: 4.1.0
|
||||
get-package-type: 0.1.0
|
||||
js-yaml: 4.3.1
|
||||
js-yaml: 4.3.0
|
||||
resolve-from: 5.0.0
|
||||
|
||||
'@istanbuljs/schema@0.1.3': {}
|
||||
@@ -12446,11 +12428,11 @@ snapshots:
|
||||
|
||||
'@types/postcss-modules-local-by-default@4.0.2':
|
||||
dependencies:
|
||||
postcss: 8.5.26
|
||||
postcss: 8.5.14
|
||||
|
||||
'@types/postcss-modules-scope@3.0.4':
|
||||
dependencies:
|
||||
postcss: 8.5.26
|
||||
postcss: 8.5.14
|
||||
|
||||
'@types/prop-types@15.7.5': {}
|
||||
|
||||
@@ -12779,7 +12761,7 @@ snapshots:
|
||||
d3-time-format: 4.1.0
|
||||
internmap: 2.0.3
|
||||
|
||||
'@vitejs/plugin-react@5.1.4(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))':
|
||||
'@vitejs/plugin-react@5.1.4(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.7)
|
||||
@@ -12787,7 +12769,7 @@ snapshots:
|
||||
'@rolldown/pluginutils': 1.0.0-rc.3
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.18.0
|
||||
vite: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
vite: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -12841,7 +12823,7 @@ snapshots:
|
||||
ajv@8.18.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-uri: 3.1.5
|
||||
fast-uri: 3.1.2
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
@@ -13152,16 +13134,16 @@ snapshots:
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
|
||||
brace-expansion@1.1.18:
|
||||
brace-expansion@1.1.15:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
concat-map: 0.0.1
|
||||
|
||||
brace-expansion@2.1.4:
|
||||
brace-expansion@2.1.1:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
brace-expansion@5.0.9:
|
||||
brace-expansion@5.0.7:
|
||||
dependencies:
|
||||
balanced-match: 4.0.4
|
||||
|
||||
@@ -13450,9 +13432,9 @@ snapshots:
|
||||
|
||||
cookie@0.7.2: {}
|
||||
|
||||
copy-anything@3.0.5:
|
||||
copy-anything@2.0.6:
|
||||
dependencies:
|
||||
is-what: 4.1.16
|
||||
is-what: 3.14.1
|
||||
|
||||
copy-text-to-clipboard@3.2.2: {}
|
||||
|
||||
@@ -13490,7 +13472,7 @@ snapshots:
|
||||
dependencies:
|
||||
env-paths: 2.2.1
|
||||
import-fresh: 3.3.1
|
||||
js-yaml: 4.3.1
|
||||
js-yaml: 4.3.0
|
||||
parse-json: 5.2.0
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
@@ -13700,11 +13682,6 @@ snapshots:
|
||||
|
||||
debounce@1.2.1: {}
|
||||
|
||||
debug@2.6.9:
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
optional: true
|
||||
|
||||
debug@3.2.7:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -14164,7 +14141,7 @@ snapshots:
|
||||
|
||||
fast-shallow-equal@1.0.0: {}
|
||||
|
||||
fast-uri@3.1.5: {}
|
||||
fast-uri@3.1.2: {}
|
||||
|
||||
fast_array_intersect@1.1.0: {}
|
||||
|
||||
@@ -14579,7 +14556,7 @@ snapshots:
|
||||
|
||||
history@5.3.0:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.28.2
|
||||
|
||||
hoist-non-react-statics@3.3.2:
|
||||
dependencies:
|
||||
@@ -14658,9 +14635,9 @@ snapshots:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
icss-utils@5.1.0(postcss@8.5.26):
|
||||
icss-utils@5.1.0(postcss@8.5.14):
|
||||
dependencies:
|
||||
postcss: 8.5.26
|
||||
postcss: 8.5.14
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
|
||||
@@ -14668,11 +14645,14 @@ snapshots:
|
||||
|
||||
ignore@7.0.5: {}
|
||||
|
||||
image-size@0.5.5:
|
||||
optional: true
|
||||
|
||||
immediate@3.0.6: {}
|
||||
|
||||
immer@11.1.3: {}
|
||||
|
||||
immutable@5.1.9: {}
|
||||
immutable@5.1.5: {}
|
||||
|
||||
import-fresh@3.3.1:
|
||||
dependencies:
|
||||
@@ -14891,7 +14871,7 @@ snapshots:
|
||||
call-bound: 1.0.4
|
||||
get-intrinsic: 1.3.0
|
||||
|
||||
is-what@4.1.16: {}
|
||||
is-what@3.14.1: {}
|
||||
|
||||
is-wsl@3.1.1:
|
||||
dependencies:
|
||||
@@ -15449,7 +15429,7 @@ snapshots:
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-yaml@4.3.1:
|
||||
js-yaml@4.3.0:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
@@ -15498,7 +15478,7 @@ snapshots:
|
||||
'@types/json-schema': 7.0.15
|
||||
'@types/lodash': 4.17.24
|
||||
is-glob: 4.0.3
|
||||
js-yaml: 4.3.1
|
||||
js-yaml: 4.3.0
|
||||
lodash: 4.18.1
|
||||
minimist: 1.2.8
|
||||
prettier: 3.8.3
|
||||
@@ -15553,17 +15533,18 @@ snapshots:
|
||||
|
||||
lerc@3.0.0: {}
|
||||
|
||||
less@4.9.0:
|
||||
less@4.4.0:
|
||||
dependencies:
|
||||
copy-anything: 3.0.5
|
||||
copy-anything: 2.0.6
|
||||
parse-node-version: 1.0.1
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
errno: 0.1.8
|
||||
graceful-fs: 4.2.11
|
||||
make-dir: 5.1.0
|
||||
image-size: 0.5.5
|
||||
make-dir: 2.1.0
|
||||
mime: 1.6.0
|
||||
needle: 3.2.0
|
||||
probe-image-size: 7.4.0
|
||||
source-map: 0.6.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -15729,13 +15710,16 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
make-dir@2.1.0:
|
||||
dependencies:
|
||||
pify: 4.0.1
|
||||
semver: 5.7.2
|
||||
optional: true
|
||||
|
||||
make-dir@3.1.0:
|
||||
dependencies:
|
||||
semver: 6.3.1
|
||||
|
||||
make-dir@5.1.0:
|
||||
optional: true
|
||||
|
||||
make-error@1.3.6: {}
|
||||
|
||||
makeerror@1.0.12:
|
||||
@@ -16139,19 +16123,19 @@ snapshots:
|
||||
|
||||
minimatch@10.2.5:
|
||||
dependencies:
|
||||
brace-expansion: 5.0.9
|
||||
brace-expansion: 5.0.7
|
||||
|
||||
minimatch@3.1.5:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.18
|
||||
brace-expansion: 1.1.15
|
||||
|
||||
minimatch@5.1.9:
|
||||
dependencies:
|
||||
brace-expansion: 2.1.4
|
||||
brace-expansion: 2.1.1
|
||||
|
||||
minimatch@9.0.9:
|
||||
dependencies:
|
||||
brace-expansion: 2.1.4
|
||||
brace-expansion: 2.1.1
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
@@ -16200,9 +16184,6 @@ snapshots:
|
||||
|
||||
mri@1.2.0: {}
|
||||
|
||||
ms@2.0.0:
|
||||
optional: true
|
||||
|
||||
ms@2.1.2: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
@@ -16253,21 +16234,12 @@ snapshots:
|
||||
dependencies:
|
||||
big-integer: 1.6.51
|
||||
|
||||
nanoid@3.3.18: {}
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
napi-postinstall@0.3.4: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
needle@2.9.1:
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
iconv-lite: 0.4.24
|
||||
sax: 1.6.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
needle@3.2.0:
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
@@ -16440,7 +16412,7 @@ snapshots:
|
||||
find-up: 8.0.0
|
||||
fs-extra: 11.3.3
|
||||
jiti: 2.6.1
|
||||
js-yaml: 4.3.1
|
||||
js-yaml: 4.3.0
|
||||
remeda: 2.34.0
|
||||
string-argv: 0.3.2
|
||||
tsconfck: 3.1.6(typescript@5.9.3)
|
||||
@@ -16639,6 +16611,9 @@ snapshots:
|
||||
|
||||
picomatch@4.0.4: {}
|
||||
|
||||
pify@4.0.1:
|
||||
optional: true
|
||||
|
||||
pirates@4.0.7: {}
|
||||
|
||||
pkg-dir@4.2.0:
|
||||
@@ -16647,37 +16622,37 @@ snapshots:
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
postcss-load-config@3.1.4(postcss@8.5.26)(ts-node@10.9.1(@types/node@16.18.25)(typescript@5.9.3)):
|
||||
postcss-load-config@3.1.4(postcss@8.5.14)(ts-node@10.9.1(@types/node@16.18.25)(typescript@5.9.3)):
|
||||
dependencies:
|
||||
lilconfig: 2.1.0
|
||||
yaml: 1.10.3
|
||||
optionalDependencies:
|
||||
postcss: 8.5.26
|
||||
postcss: 8.5.14
|
||||
ts-node: 10.9.1(@types/node@16.18.25)(typescript@5.9.3)
|
||||
|
||||
postcss-modules-extract-imports@3.0.0(postcss@8.5.26):
|
||||
postcss-modules-extract-imports@3.0.0(postcss@8.5.14):
|
||||
dependencies:
|
||||
postcss: 8.5.26
|
||||
postcss: 8.5.14
|
||||
|
||||
postcss-modules-local-by-default@4.2.0(postcss@8.5.26):
|
||||
postcss-modules-local-by-default@4.2.0(postcss@8.5.14):
|
||||
dependencies:
|
||||
icss-utils: 5.1.0(postcss@8.5.26)
|
||||
postcss: 8.5.26
|
||||
icss-utils: 5.1.0(postcss@8.5.14)
|
||||
postcss: 8.5.14
|
||||
postcss-selector-parser: 7.1.1
|
||||
postcss-value-parser: 4.2.0
|
||||
|
||||
postcss-modules-scope@3.2.1(postcss@8.5.26):
|
||||
postcss-modules-scope@3.2.1(postcss@8.5.14):
|
||||
dependencies:
|
||||
postcss: 8.5.26
|
||||
postcss: 8.5.14
|
||||
postcss-selector-parser: 7.1.1
|
||||
|
||||
postcss-safe-parser@7.0.1(postcss@8.5.26):
|
||||
postcss-safe-parser@7.0.1(postcss@8.5.14):
|
||||
dependencies:
|
||||
postcss: 8.5.26
|
||||
postcss: 8.5.14
|
||||
|
||||
postcss-scss@4.0.9(postcss@8.5.26):
|
||||
postcss-scss@4.0.9(postcss@8.5.14):
|
||||
dependencies:
|
||||
postcss: 8.5.26
|
||||
postcss: 8.5.14
|
||||
|
||||
postcss-selector-parser@7.1.1:
|
||||
dependencies:
|
||||
@@ -16686,9 +16661,9 @@ snapshots:
|
||||
|
||||
postcss-value-parser@4.2.0: {}
|
||||
|
||||
postcss@8.5.26:
|
||||
postcss@8.5.14:
|
||||
dependencies:
|
||||
nanoid: 3.3.18
|
||||
nanoid: 3.3.11
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
@@ -16739,15 +16714,6 @@ snapshots:
|
||||
|
||||
prismjs@1.30.0: {}
|
||||
|
||||
probe-image-size@7.4.0:
|
||||
dependencies:
|
||||
lodash.merge: 4.6.2
|
||||
needle: 2.9.1
|
||||
stream-parser: 0.3.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
progress@2.0.3: {}
|
||||
@@ -17650,13 +17616,13 @@ snapshots:
|
||||
|
||||
robust-predicates@3.0.2: {}
|
||||
|
||||
rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4):
|
||||
rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4):
|
||||
dependencies:
|
||||
'@oxc-project/runtime': 0.101.0
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
lightningcss: 1.31.1
|
||||
picomatch: 4.0.4
|
||||
postcss: 8.5.26
|
||||
postcss: 8.5.14
|
||||
rolldown: 1.0.0-beta.53
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
@@ -17664,7 +17630,7 @@ snapshots:
|
||||
esbuild: 0.28.1
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.6.1
|
||||
less: 4.9.0
|
||||
less: 4.4.0
|
||||
sass: 1.97.3
|
||||
stylus: 0.62.0
|
||||
terser: 5.46.2
|
||||
@@ -17739,7 +17705,7 @@ snapshots:
|
||||
sass@1.97.3:
|
||||
dependencies:
|
||||
chokidar: 4.0.3
|
||||
immutable: 5.1.9
|
||||
immutable: 5.1.5
|
||||
source-map-js: 1.2.1
|
||||
optionalDependencies:
|
||||
'@parcel/watcher': 2.5.1
|
||||
@@ -17769,6 +17735,9 @@ snapshots:
|
||||
refa: 0.12.1
|
||||
regexp-ast-analysis: 0.7.1
|
||||
|
||||
semver@5.7.2:
|
||||
optional: true
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.8.5: {}
|
||||
@@ -17955,13 +17924,6 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
internal-slot: 1.1.0
|
||||
|
||||
stream-parser@0.3.1:
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
strict-event-emitter@0.2.8:
|
||||
dependencies:
|
||||
events: 3.3.0
|
||||
@@ -18083,8 +18045,8 @@ snapshots:
|
||||
micromatch: 4.0.8
|
||||
normalize-path: 3.0.0
|
||||
picocolors: 1.1.1
|
||||
postcss: 8.5.26
|
||||
postcss-safe-parser: 7.0.1(postcss@8.5.26)
|
||||
postcss: 8.5.14
|
||||
postcss-safe-parser: 7.0.1(postcss@8.5.14)
|
||||
postcss-selector-parser: 7.1.1
|
||||
postcss-value-parser: 4.2.0
|
||||
string-width: 8.2.0
|
||||
@@ -18325,14 +18287,14 @@ snapshots:
|
||||
'@types/postcss-modules-local-by-default': 4.0.2
|
||||
'@types/postcss-modules-scope': 3.0.4
|
||||
dotenv: 16.6.1
|
||||
icss-utils: 5.1.0(postcss@8.5.26)
|
||||
less: 4.9.0
|
||||
icss-utils: 5.1.0(postcss@8.5.14)
|
||||
less: 4.4.0
|
||||
lodash.camelcase: 4.3.0
|
||||
postcss: 8.5.26
|
||||
postcss-load-config: 3.1.4(postcss@8.5.26)(ts-node@10.9.1(@types/node@16.18.25)(typescript@5.9.3))
|
||||
postcss-modules-extract-imports: 3.0.0(postcss@8.5.26)
|
||||
postcss-modules-local-by-default: 4.2.0(postcss@8.5.26)
|
||||
postcss-modules-scope: 3.2.1(postcss@8.5.26)
|
||||
postcss: 8.5.14
|
||||
postcss-load-config: 3.1.4(postcss@8.5.14)(ts-node@10.9.1(@types/node@16.18.25)(typescript@5.9.3))
|
||||
postcss-modules-extract-imports: 3.0.0(postcss@8.5.14)
|
||||
postcss-modules-local-by-default: 4.2.0(postcss@8.5.14)
|
||||
postcss-modules-scope: 3.2.1(postcss@8.5.14)
|
||||
reserved-words: 0.1.2
|
||||
sass: 1.97.3
|
||||
source-map-js: 1.2.1
|
||||
@@ -18557,7 +18519,7 @@ snapshots:
|
||||
unist-util-stringify-position: 4.0.0
|
||||
vfile-message: 4.0.2
|
||||
|
||||
vite-plugin-checker@0.12.0(eslint@10.2.1(jiti@2.6.1))(meow@13.2.0)(optionator@0.9.4)(oxlint@1.69.0(oxlint-tsgolint@0.23.0))(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(stylelint@17.7.0(typescript@5.9.3))(typescript@5.9.3):
|
||||
vite-plugin-checker@0.12.0(eslint@10.2.1(jiti@2.6.1))(meow@13.2.0)(optionator@0.9.4)(oxlint@1.69.0(oxlint-tsgolint@0.23.0))(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(stylelint@17.7.0(typescript@5.9.3))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.0
|
||||
chokidar: 4.0.3
|
||||
@@ -18566,7 +18528,7 @@ snapshots:
|
||||
picomatch: 4.0.4
|
||||
tiny-invariant: 1.3.3
|
||||
tinyglobby: 0.2.15
|
||||
vite: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
vite: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
vscode-uri: 3.1.0
|
||||
optionalDependencies:
|
||||
eslint: 10.2.1(jiti@2.6.1)
|
||||
@@ -18576,16 +18538,16 @@ snapshots:
|
||||
stylelint: 17.7.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
|
||||
vite-plugin-compression@0.5.1(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)):
|
||||
vite-plugin-compression@0.5.1(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)):
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
debug: 4.3.4
|
||||
fs-extra: 10.1.0
|
||||
vite: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
vite: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-html@3.2.2(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)):
|
||||
vite-plugin-html@3.2.2(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
colorette: 2.0.20
|
||||
@@ -18599,23 +18561,23 @@ snapshots:
|
||||
html-minifier-terser: 6.1.0
|
||||
node-html-parser: 5.4.2
|
||||
pathe: 0.2.0
|
||||
vite: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
vite: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
|
||||
vite-plugin-image-optimizer@2.0.3(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(sharp@0.35.0)(svgo@4.0.2):
|
||||
vite-plugin-image-optimizer@2.0.3(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(sharp@0.35.0)(svgo@4.0.2):
|
||||
dependencies:
|
||||
ansi-colors: 4.1.3
|
||||
pathe: 2.0.3
|
||||
vite: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
vite: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
optionalDependencies:
|
||||
sharp: 0.35.0
|
||||
svgo: 4.0.2
|
||||
|
||||
vite-tsconfig-paths@6.1.1(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(typescript@5.9.3):
|
||||
vite-tsconfig-paths@6.1.1(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4))(typescript@5.9.3):
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
globrex: 0.1.2
|
||||
tsconfck: 3.1.6(typescript@5.9.3)
|
||||
vite: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
vite: rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.8.4)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
@@ -13,16 +13,7 @@ overrides:
|
||||
'@babel/core@<=7.29.0': '>=7.29.6 <8'
|
||||
# via: jest > babel-plugin-istanbul > @istanbuljs/load-nyc-config@1.1.0 (js-yaml ^3.13.1)
|
||||
# remove: blocked — 1.1.0 is latest and still depends on js-yaml 3.x
|
||||
'@istanbuljs/load-nyc-config>js-yaml': '>=4.3.1 <5'
|
||||
# via: babel-plugin-istanbul > test-exclude@6 > minimatch@3.1.5 (^1.1.7); also glob@7
|
||||
# remove: blocked — babel-plugin-istanbul pins test-exclude@6, which pins minimatch@3
|
||||
brace-expansion@<1.1.18: '>=1.1.18 <2'
|
||||
# via: jest > glob@10 > minimatch@9.0.9 (^2.0.1); also minimatch@5.1.9 (^2.0.1)
|
||||
# remove: blocked — jest 30 resolves glob@10 > minimatch@9 internally
|
||||
'brace-expansion@>=2.0.0 <2.1.4': '>=2.1.4 <3'
|
||||
# via: eslint-plugin-sonarjs@4.0.2 (minimatch ^10.2.4) > minimatch@10.2.5 (^5.0.5)
|
||||
# remove: blocked — minimatch@10.2.6 (latest) only widens to ^5.0.8, still vulnerable
|
||||
'brace-expansion@>=5.0.0 <5.0.9': '>=5.0.9 <6'
|
||||
'@istanbuljs/load-nyc-config>js-yaml': '>=4.2.0 <5'
|
||||
# via: msw@1.3.2 (devDep) > cookie ^0.4.2
|
||||
# remove: upgrade msw to >=2 (ships cookie ^1). Do NOT open the cap: cookie >=1 is
|
||||
# ESM-only and breaks msw under jest's CJS sandbox (kills every test suite)
|
||||
@@ -36,26 +27,13 @@ overrides:
|
||||
# remove: blocked on rolldown-vite (7.3.1 is latest, still ^0.27.0);
|
||||
# orval >=8.20.0 and ts-jest >=29.4.11 already fixed on their side
|
||||
esbuild@>=0.27.3 <0.28.1: '>=0.28.1 <0.29.0'
|
||||
# via: @commitlint/cli > @commitlint/config-validator > ajv@8 (fast-uri ^3.0.1)
|
||||
# remove: blocked — ajv@8 caps fast-uri at ^3, and only 3.1.5 carries the fix
|
||||
fast-uri@<3.1.5: '>=3.1.5 <4'
|
||||
# via: direct devDep sass@1.97.3 (immutable ^5.0.2)
|
||||
# remove: blocked — plain sass bumps stay within ^5, so the floor is what pulls 5.1.8
|
||||
immutable@<5.1.8: '>=5.1.8 <6'
|
||||
# via: react-use@17.5.1 (direct, js-cookie ^2.2.1); @grafana/data > react-use@17.6.0
|
||||
# remove: bump react-use to >=17.6.1 (js-cookie ^3); @grafana/data side blocked
|
||||
js-cookie@<=3.0.5: '>=3.0.7 <4'
|
||||
# via: @orval/core@8.9.1 (devDep, js-yaml 4.1.1 EXACT pin — not deletable);
|
||||
# json-schema-to-typescript@15 > @apidevtools/json-schema-ref-parser (^4.1.0)
|
||||
# remove: upgrade orval to >=8.20.0 (drops js-yaml dependency entirely)
|
||||
'js-yaml@>=4.0.0 <4.3.1': '>=4.3.1 <5'
|
||||
# via: typescript-plugin-css-modules@5.2.0 (less ^4.2.0) > less@4.4.0 (image-size ~0.5.0)
|
||||
# remove: bump typescript-plugin-css-modules once it floors less itself; image-size has
|
||||
# no patched release at all, so dropping the dep is the only fix — less@4.5.0 did
|
||||
less@<4.5.0: '>=4.5.0 <5'
|
||||
# via: direct dep postcss (nanoid ^3.3.17)
|
||||
# remove: blocked — postcss 8.5.26 (latest) caps nanoid at ^3, fix landed in 3.3.18
|
||||
nanoid@<3.3.18: '>=3.3.18 <4'
|
||||
js-yaml@>=4.0.0 <=4.1.1: '>=4.2.0 <5'
|
||||
# via: react-syntax-highlighter@15.5.0 (prismjs ^1.27.0 + refractor@3 ~1.27.0 tilde-pinned)
|
||||
# remove: bump react-syntax-highlighter to >=16.1.1 (prismjs ^1.30.0, refractor@5)
|
||||
prismjs@<1.30.0: '>=1.30.0 <2'
|
||||
|
||||
@@ -8,19 +8,12 @@ import {
|
||||
|
||||
import ChangelogRenderer from '../components/ChangelogRenderer';
|
||||
|
||||
// Mock react-markdown to render children as plain text and a sample
|
||||
// anchor through the `components.a` override
|
||||
// Mock react-markdown to just render children as plain text
|
||||
jest.mock(
|
||||
'react-markdown',
|
||||
() =>
|
||||
function ReactMarkdown({ children, components }: any) {
|
||||
const Anchor = components?.a;
|
||||
return (
|
||||
<div>
|
||||
{children}
|
||||
{Anchor && <Anchor href="https://signoz.io/docs">docs</Anchor>}
|
||||
</div>
|
||||
);
|
||||
function ReactMarkdown({ children }: any) {
|
||||
return <div>{children}</div>;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -69,14 +62,4 @@ describe('ChangelogRenderer', () => {
|
||||
expect(screen.getByAltText('Media')).toBeInTheDocument();
|
||||
expect(screen.getByText('Description for feature 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders markdown links that open in a new tab', () => {
|
||||
render(<ChangelogRenderer changelog={mockChangelog} />);
|
||||
const links = screen.getAllByRole('link', { name: 'docs' });
|
||||
expect(links.length).toBeGreaterThan(0);
|
||||
links.forEach((link) => {
|
||||
expect(link).toHaveAttribute('target', '_blank');
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,19 +13,6 @@ interface Props {
|
||||
changelog: ChangelogSchema;
|
||||
}
|
||||
|
||||
interface LinkProps {
|
||||
href?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
function Link({ href, children }: LinkProps): JSX.Element {
|
||||
return (
|
||||
<a href={href} target="_blank" rel="noopener noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function renderMedia(media: Media): JSX.Element | null {
|
||||
if (SupportedImageTypes.includes(media.ext)) {
|
||||
return (
|
||||
@@ -75,9 +62,7 @@ function ChangelogRenderer({ changelog }: Props): JSX.Element {
|
||||
<div key={feature.id}>
|
||||
<div className="changelog-renderer-section-title">{feature.title}</div>
|
||||
{feature.media && renderMedia(feature.media)}
|
||||
<ReactMarkdown components={{ a: Link }}>
|
||||
{feature.description}
|
||||
</ReactMarkdown>
|
||||
<ReactMarkdown>{feature.description}</ReactMarkdown>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -86,9 +71,7 @@ function ChangelogRenderer({ changelog }: Props): JSX.Element {
|
||||
<div className="changelog-renderer-bug-fixes">
|
||||
<div className="changelog-renderer-section-title">Bug Fixes</div>
|
||||
{changelog.bug_fixes && (
|
||||
<ReactMarkdown components={{ a: Link }}>
|
||||
{changelog.bug_fixes}
|
||||
</ReactMarkdown>
|
||||
<ReactMarkdown>{changelog.bug_fixes}</ReactMarkdown>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -96,9 +79,7 @@ function ChangelogRenderer({ changelog }: Props): JSX.Element {
|
||||
<div className="changelog-renderer-maintenance">
|
||||
<div className="changelog-renderer-section-title">Maintenance</div>
|
||||
{changelog.maintenance && (
|
||||
<ReactMarkdown components={{ a: Link }}>
|
||||
{changelog.maintenance}
|
||||
</ReactMarkdown>
|
||||
<ReactMarkdown>{changelog.maintenance}</ReactMarkdown>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -28,6 +28,9 @@ interface FieldsSelectorProps {
|
||||
signal: DataSource;
|
||||
maxFields?: number;
|
||||
requiredFields?: readonly string[];
|
||||
// Lets users add a free-typed field which
|
||||
// does not show up in the suggestions
|
||||
allowCustomFields?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
defaultPosition?: { x: number; y: number };
|
||||
@@ -46,6 +49,7 @@ function FieldsSelectorContent({
|
||||
signal,
|
||||
maxFields,
|
||||
requiredFields,
|
||||
allowCustomFields,
|
||||
width = DEFAULT_PANEL_WIDTH,
|
||||
height,
|
||||
defaultPosition,
|
||||
@@ -67,7 +71,7 @@ function FieldsSelectorContent({
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const value = e.target.value.trim().toLowerCase();
|
||||
const value = e.target.value.trim();
|
||||
setInputValue(value);
|
||||
debouncedUpdate(value);
|
||||
},
|
||||
@@ -153,6 +157,7 @@ function FieldsSelectorContent({
|
||||
addedFields={draftFields}
|
||||
onAdd={handleAdd}
|
||||
isAtLimit={isAtLimit}
|
||||
allowCustomFields={allowCustomFields}
|
||||
/>
|
||||
|
||||
{hasUnsavedChanges && (
|
||||
@@ -192,7 +197,7 @@ function FieldsSelector({
|
||||
() =>
|
||||
fields.map((f) => ({
|
||||
...f,
|
||||
key: f.key ?? buildCompositeKey(f.name, f.fieldContext),
|
||||
key: buildCompositeKey(f.name, f.fieldContext),
|
||||
})),
|
||||
[fields],
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ interface OtherFieldsProps {
|
||||
addedFields: TelemetryFieldKey[];
|
||||
onAdd: (field: TelemetryFieldKey) => void;
|
||||
isAtLimit: boolean;
|
||||
allowCustomFields?: boolean;
|
||||
}
|
||||
|
||||
function OtherFields({
|
||||
@@ -29,6 +30,7 @@ function OtherFields({
|
||||
addedFields,
|
||||
onAdd,
|
||||
isAtLimit,
|
||||
allowCustomFields,
|
||||
}: OtherFieldsProps): JSX.Element {
|
||||
const { data, isFetching } = useGetQueryKeySuggestions(
|
||||
{
|
||||
@@ -45,25 +47,45 @@ function OtherFields({
|
||||
},
|
||||
);
|
||||
|
||||
const otherFields: TelemetryFieldKey[] = useMemo(() => {
|
||||
const suggestions = Object.values(data?.data.data.keys || {}).flat();
|
||||
const otherFields = useMemo<TelemetryFieldKey[]>(() => {
|
||||
const rawSuggestions = Object.values(data?.data.data.keys || {}).flat();
|
||||
// Normalize: synthesize `key` once so downstream reads can trust it.
|
||||
const normalizedSuggestions: TelemetryFieldKey[] = suggestions.map(
|
||||
(attr) => ({
|
||||
...attr,
|
||||
key: buildCompositeKey(attr.name, attr.fieldContext as string),
|
||||
signal: attr.signal as SignalType,
|
||||
fieldContext: attr.fieldContext as FieldContext,
|
||||
fieldDataType: attr.fieldDataType,
|
||||
}),
|
||||
);
|
||||
const suggestions: TelemetryFieldKey[] = rawSuggestions.map((attr) => ({
|
||||
...attr,
|
||||
key: buildCompositeKey(attr.name, attr.fieldContext as string),
|
||||
signal: attr.signal as SignalType,
|
||||
fieldContext: attr.fieldContext as FieldContext,
|
||||
fieldDataType: attr.fieldDataType,
|
||||
}));
|
||||
const addedIds = new Set(
|
||||
addedFields.map((f) => f.key ?? buildCompositeKey(f.name, f.fieldContext)),
|
||||
addedFields.map((f) => buildCompositeKey(f.name, f.fieldContext)),
|
||||
);
|
||||
return normalizedSuggestions.filter(
|
||||
const available = suggestions.filter(
|
||||
(attr) => !addedIds.has(attr.key as string),
|
||||
);
|
||||
}, [data, addedFields]);
|
||||
|
||||
// Prepend the custom field when its name is not in suggestions and
|
||||
// not already added.
|
||||
const typed = debouncedInputValue.trim();
|
||||
const nameMatches = (list: TelemetryFieldKey[]): boolean =>
|
||||
list.some((f) => f.name.toLowerCase() === typed.toLowerCase());
|
||||
const showCustom =
|
||||
!!allowCustomFields &&
|
||||
typed.length > 0 &&
|
||||
!nameMatches(suggestions) &&
|
||||
!nameMatches(addedFields);
|
||||
|
||||
if (!showCustom) {
|
||||
return available;
|
||||
}
|
||||
const customField: TelemetryFieldKey = {
|
||||
name: typed,
|
||||
fieldContext: '',
|
||||
fieldDataType: '',
|
||||
key: buildCompositeKey(typed, ''),
|
||||
};
|
||||
return [customField, ...available];
|
||||
}, [data, addedFields, allowCustomFields, debouncedInputValue]);
|
||||
|
||||
if (isFetching) {
|
||||
return (
|
||||
|
||||
@@ -11,7 +11,7 @@ const makeField = (name: string, fieldContext = 'log'): TelemetryFieldKey => ({
|
||||
signal: 'logs',
|
||||
fieldContext: fieldContext as TelemetryFieldKey['fieldContext'],
|
||||
fieldDataType: 'string',
|
||||
key: `${fieldContext}.${name}`,
|
||||
key: `${fieldContext}:${name}`,
|
||||
});
|
||||
|
||||
describe('AddedFields — requiredFields', () => {
|
||||
@@ -33,7 +33,7 @@ describe('AddedFields — requiredFields', () => {
|
||||
inputValue=""
|
||||
fields={fields}
|
||||
onFieldsChange={jest.fn()}
|
||||
requiredFields={['log.a', 'log.c']}
|
||||
requiredFields={['log:a', 'log:c']}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('AddedFields — requiredFields', () => {
|
||||
inputValue=""
|
||||
fields={fields}
|
||||
onFieldsChange={jest.fn()}
|
||||
requiredFields={['log.a']}
|
||||
requiredFields={['log:a']}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('AddedFields — requiredFields', () => {
|
||||
inputValue=""
|
||||
fields={fields}
|
||||
onFieldsChange={jest.fn()}
|
||||
requiredFields={['log.body']}
|
||||
requiredFields={['log:body']}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -101,11 +101,11 @@ describe('AddedFields — requiredFields', () => {
|
||||
inputValue=""
|
||||
fields={fields}
|
||||
onFieldsChange={jest.fn()}
|
||||
requiredFields={['log.body']}
|
||||
requiredFields={['log:body']}
|
||||
/>,
|
||||
);
|
||||
|
||||
// 'log.body' locked, 'log.body_extra' removable.
|
||||
// 'log:body' locked, 'log:body_extra' removable.
|
||||
expect(screen.getAllByRole('button', { name: /remove/i })).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import { act, fireEvent, render, screen } from 'tests/test-utils';
|
||||
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import FieldsSelector from '../FieldsSelector';
|
||||
|
||||
jest.mock('hooks/querySuggestions/useGetQueryKeySuggestions');
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: { success: jest.fn(), error: jest.fn() },
|
||||
}));
|
||||
|
||||
// FloatingPanel is a react-rnd/portal shell — presentation only. Render its
|
||||
// children directly so the test exercises the column-editing behavior.
|
||||
jest.mock('periscope/components/FloatingPanel', () => ({
|
||||
FloatingPanel: ({ children }: { children: React.ReactNode }): JSX.Element => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockSuggestions = (names: string[]): void => {
|
||||
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
data: {
|
||||
data: {
|
||||
keys: {
|
||||
attributeKeys: names.map((name) => ({
|
||||
name,
|
||||
signal: 'logs',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: '',
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
isFetching: false,
|
||||
});
|
||||
};
|
||||
|
||||
const field = (name: string, fieldContext = 'log'): TelemetryFieldKey => ({
|
||||
name,
|
||||
signal: 'logs',
|
||||
fieldContext: fieldContext as TelemetryFieldKey['fieldContext'],
|
||||
fieldDataType: 'string',
|
||||
});
|
||||
|
||||
const renderPanel = (
|
||||
props: Partial<React.ComponentProps<typeof FieldsSelector>> = {},
|
||||
): { onFieldsChange: jest.Mock } => {
|
||||
const onFieldsChange = jest.fn();
|
||||
render(
|
||||
<FieldsSelector
|
||||
isOpen
|
||||
title="Edit columns"
|
||||
fields={props.fields ?? []}
|
||||
onFieldsChange={onFieldsChange}
|
||||
onClose={jest.fn()}
|
||||
signal={DataSource.LOGS}
|
||||
allowCustomFields
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
return { onFieldsChange };
|
||||
};
|
||||
|
||||
// Type into the search box and flush the 400ms debounce so OtherFields (driven
|
||||
// by the debounced value) recomputes.
|
||||
const typeSearch = (value: string): void => {
|
||||
const input = screen.getByPlaceholderText('Search for a field...');
|
||||
act(() => {
|
||||
fireEvent.change(input, { target: { value } });
|
||||
});
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(400);
|
||||
});
|
||||
};
|
||||
|
||||
describe('FieldsSelector — edit columns (integration)', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
mockSuggestions([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('adds a free-typed field end to end and saves the synthesized key', () => {
|
||||
const { onFieldsChange } = renderPanel({ fields: [field('body')] });
|
||||
|
||||
typeSearch('orderId');
|
||||
|
||||
// custom option surfaces in OTHER FIELDS (only Add button, no suggestions)
|
||||
expect(screen.getByText('orderId')).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /^add$/i }));
|
||||
});
|
||||
|
||||
// moved into ADDED FIELDS → OTHER FIELDS has nothing left to offer
|
||||
expect(screen.getByText('No values found')).toBeInTheDocument();
|
||||
|
||||
// Save commits the draft
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /save changes/i }));
|
||||
});
|
||||
|
||||
expect(onFieldsChange).toHaveBeenCalledTimes(1);
|
||||
const saved = onFieldsChange.mock.calls[0][0] as TelemetryFieldKey[];
|
||||
expect(saved).toStrictEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'orderId',
|
||||
fieldContext: '',
|
||||
fieldDataType: '',
|
||||
key: 'orderId',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('adds a suggested field: it moves from OTHER FIELDS into ADDED FIELDS', () => {
|
||||
mockSuggestions(['service.name']);
|
||||
const { onFieldsChange } = renderPanel({ fields: [] });
|
||||
|
||||
const addButton = screen.getByRole('button', { name: /^add$/i });
|
||||
act(() => {
|
||||
fireEvent.click(addButton);
|
||||
});
|
||||
|
||||
// now removable in ADDED FIELDS, no longer offered in OTHER FIELDS
|
||||
expect(screen.getByRole('button', { name: /remove/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /^add$/i }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /save changes/i }));
|
||||
});
|
||||
const saved = onFieldsChange.mock.calls[0][0] as TelemetryFieldKey[];
|
||||
expect(saved.map((f) => f.name)).toContain('service.name');
|
||||
});
|
||||
|
||||
it('hides the custom option when the typed name is already added', () => {
|
||||
renderPanel({ fields: [field('orderId')] });
|
||||
|
||||
typeSearch('ORDERID');
|
||||
|
||||
// exact name already added → nothing left to offer in OTHER FIELDS
|
||||
expect(screen.queryByText('ORDERID')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('No values found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not offer a custom option when allowCustomFields is off', () => {
|
||||
renderPanel({ fields: [], allowCustomFields: false });
|
||||
|
||||
typeSearch('unknown.a.b.c');
|
||||
|
||||
// no custom row and nothing addable
|
||||
expect(screen.queryByText('unknown.a.b.c')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /^add$/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('discards an added field, reverting the draft', () => {
|
||||
const { onFieldsChange } = renderPanel({ fields: [field('body')] });
|
||||
|
||||
typeSearch('orderId');
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /^add$/i }));
|
||||
});
|
||||
|
||||
// clear the search so the added list is not filtered
|
||||
typeSearch('');
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /discard/i }));
|
||||
});
|
||||
|
||||
expect(screen.queryByText('orderId')).not.toBeInTheDocument();
|
||||
expect(onFieldsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import { fireEvent, render, screen } from 'tests/test-utils';
|
||||
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import OtherFields from '../OtherFields';
|
||||
|
||||
jest.mock('hooks/querySuggestions/useGetQueryKeySuggestions');
|
||||
|
||||
const mockSuggestions = (names: string[]): void => {
|
||||
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
data: {
|
||||
data: {
|
||||
keys: {
|
||||
attributeKeys: names.map((name) => ({
|
||||
name,
|
||||
signal: 'logs',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: '',
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
isFetching: false,
|
||||
});
|
||||
};
|
||||
|
||||
const renderOtherFields = (
|
||||
props: Partial<React.ComponentProps<typeof OtherFields>> = {},
|
||||
): { onAdd: jest.Mock } => {
|
||||
const onAdd = jest.fn();
|
||||
render(
|
||||
<OtherFields
|
||||
signal={DataSource.LOGS}
|
||||
debouncedInputValue=""
|
||||
addedFields={[]}
|
||||
onAdd={onAdd}
|
||||
isAtLimit={false}
|
||||
allowCustomFields
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
return { onAdd };
|
||||
};
|
||||
|
||||
const addedField = (name: string): TelemetryFieldKey => ({
|
||||
name,
|
||||
signal: 'logs',
|
||||
fieldContext: '',
|
||||
fieldDataType: '',
|
||||
key: name,
|
||||
});
|
||||
|
||||
describe('OtherFields — custom (free-typed) option', () => {
|
||||
beforeEach(() => {
|
||||
mockSuggestions([]);
|
||||
});
|
||||
|
||||
it('shows a custom option for a typed name that is not a suggestion', () => {
|
||||
renderOtherFields({ debouncedInputValue: 'unknown.a.b.c' });
|
||||
|
||||
expect(screen.getByText('unknown.a.b.c')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /add/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('synthesizes the field with raw name, empty context/type, on add', () => {
|
||||
const { onAdd } = renderOtherFields({ debouncedInputValue: 'orderId' });
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /add/i }));
|
||||
|
||||
expect(onAdd).toHaveBeenCalledWith({
|
||||
name: 'orderId',
|
||||
fieldContext: '',
|
||||
fieldDataType: '',
|
||||
key: 'orderId',
|
||||
});
|
||||
});
|
||||
|
||||
it('hides the custom option when an exact suggestion exists (case-insensitive)', () => {
|
||||
mockSuggestions(['orderId']);
|
||||
renderOtherFields({ debouncedInputValue: 'orderid' });
|
||||
|
||||
// the real suggestion shows, the lowercased custom name does not
|
||||
expect(screen.getByText('orderId')).toBeInTheDocument();
|
||||
expect(screen.queryByText('orderid')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides the custom option when the name is already added (case-insensitive)', () => {
|
||||
renderOtherFields({
|
||||
debouncedInputValue: 'ORDERID',
|
||||
addedFields: [addedField('orderId')],
|
||||
});
|
||||
|
||||
expect(screen.queryByText('ORDERID')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('No values found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show the custom option when allowCustomFields is off', () => {
|
||||
renderOtherFields({
|
||||
debouncedInputValue: 'unknown.a.b.c',
|
||||
allowCustomFields: false,
|
||||
});
|
||||
|
||||
expect(screen.queryByText('unknown.a.b.c')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('No values found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show the custom option for an empty input', () => {
|
||||
renderOtherFields({ debouncedInputValue: ' ' });
|
||||
|
||||
expect(screen.getByText('No values found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the custom option at the field limit but hides its Add button', () => {
|
||||
renderOtherFields({ debouncedInputValue: 'unknown.a.b.c', isAtLimit: true });
|
||||
|
||||
// same as every other row at the limit: name shown, no Add button
|
||||
expect(screen.getByText('unknown.a.b.c')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /add/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,8 @@
|
||||
.ant-drawer-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 16px;
|
||||
padding: 16px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -78,7 +79,6 @@
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
margin-top: 16px;
|
||||
|
||||
.log-body {
|
||||
font-family: 'SF Mono';
|
||||
@@ -123,19 +123,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.log-detail-drawer__section-divider {
|
||||
height: 8px;
|
||||
margin: 12px 0;
|
||||
background-image:
|
||||
radial-gradient(circle, var(--l3-border) 1px, transparent 1px),
|
||||
radial-gradient(circle, var(--l3-border) 1px, transparent 1px);
|
||||
background-size: 6px 2px;
|
||||
background-position:
|
||||
left top,
|
||||
left bottom;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
|
||||
.tabs-and-search {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -11,13 +11,6 @@ jest.mock('@signozhq/ui/sonner', () => ({
|
||||
toast: { success: jest.fn(), error: jest.fn() },
|
||||
}));
|
||||
|
||||
// DataViewer pulls in react-json-tree (ESM) + Monaco; mock it (as trace's tests
|
||||
// do). These drawer tests assert the header/highlights, not the Overview body.
|
||||
jest.mock('periscope/components/DataViewer', () => ({
|
||||
__esModule: true,
|
||||
DataViewer: (): JSX.Element => <div data-testid="overview-data-viewer" />,
|
||||
}));
|
||||
|
||||
// The flag to be removed later
|
||||
jest.mock('../constants', () => ({
|
||||
...jest.requireActual('../constants'),
|
||||
@@ -73,12 +66,6 @@ describe('LogDetail drawer — header (isLogDetailsV2)', () => {
|
||||
expect(screen.getByTestId('log-details-header-next')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the DataViewer in the Overview tab', () => {
|
||||
renderDrawer();
|
||||
|
||||
expect(screen.getByTestId('overview-data-viewer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the log timestamp formatted (DASH_DATETIME) in the header', () => {
|
||||
// Pin the timezone to UTC so the formatted output is deterministic across
|
||||
// machines/CI (Jest doesn't fix a TZ).
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// temporary flag to be removed with old log details code.
|
||||
export const isLogDetailsV2 = true;
|
||||
import getLocalStorage from 'api/browser/localstorage/get';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
|
||||
// Temp feature flag before actual roll-out
|
||||
export const isLogDetailsV2 =
|
||||
getLocalStorage(LOCALSTORAGE.LOG_DETAILS_V2) === 'true';
|
||||
|
||||
export const VIEW_TYPES = {
|
||||
OVERVIEW: 'OVERVIEW',
|
||||
|
||||
@@ -100,7 +100,6 @@ function LogDetailInner({
|
||||
// Don't close if clicking on drawer content, overlays, or portal elements
|
||||
if (
|
||||
target.closest('[data-log-detail-ignore="true"]') ||
|
||||
target.closest('.log-detail-drawer') ||
|
||||
target.closest('.cm-tooltip-autocomplete') ||
|
||||
target.closest('.drawer-popover') ||
|
||||
target.closest('.query-status-popover') ||
|
||||
@@ -403,8 +402,6 @@ function LogDetailInner({
|
||||
|
||||
{isLogDetailsV2 && <LogHighlights log={log} />}
|
||||
|
||||
{isLogDetailsV2 && <div className="log-detail-drawer__section-divider" />}
|
||||
|
||||
<div className="tabs-and-search">
|
||||
<ToggleGroupSimple
|
||||
type="single"
|
||||
@@ -421,21 +418,15 @@ function LogDetailInner({
|
||||
</div>
|
||||
),
|
||||
},
|
||||
// V2's DataViewer has its own Pretty/JSON toggle, so the separate
|
||||
// JSON tab is redundant.
|
||||
...(isLogDetailsV2
|
||||
? []
|
||||
: [
|
||||
{
|
||||
value: VIEW_TYPES.JSON,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<Braces size={14} />
|
||||
JSON
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]),
|
||||
{
|
||||
value: VIEW_TYPES.JSON,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<Braces size={14} />
|
||||
JSON
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: VIEW_TYPES.CONTEXT,
|
||||
label: (
|
||||
@@ -518,7 +509,7 @@ function LogDetailInner({
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
)}
|
||||
{!isLogDetailsV2 && selectedView === VIEW_TYPES.JSON && (
|
||||
{selectedView === VIEW_TYPES.JSON && (
|
||||
<JsonView data={LogJsonData} height="68vh" />
|
||||
)}
|
||||
|
||||
|
||||
@@ -51,13 +51,13 @@ describe('useLogsTableColumns — selectColumns-order respected', () => {
|
||||
);
|
||||
|
||||
// body/timestamp appear where the caller placed them, keyed by their
|
||||
// composite IDs ('log.*'); contextless user fields collapse to bare name.
|
||||
// composite IDs ('log:*'); contextless user fields collapse to bare name.
|
||||
expect(result.current.map((c) => c.id)).toStrictEqual([
|
||||
'state-indicator',
|
||||
'service.name',
|
||||
'log.body',
|
||||
'log:body',
|
||||
'request.id',
|
||||
'log.timestamp',
|
||||
'log:timestamp',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -70,14 +70,14 @@ describe('useLogsTableColumns — selectColumns-order respected', () => {
|
||||
);
|
||||
|
||||
const byId = new Map(result.current.map((c) => [c.id, c]));
|
||||
// Attribute variant is its own column, not a duplicate 'log.body'.
|
||||
// Attribute variant is its own column, not a duplicate 'log:body'.
|
||||
expect(result.current.map((c) => c.id)).toStrictEqual([
|
||||
'state-indicator',
|
||||
'log.body',
|
||||
'attribute.body',
|
||||
'log:body',
|
||||
'attribute:body',
|
||||
]);
|
||||
expect(byId.get('log.body')?.enableRemove).toBe(false);
|
||||
expect(byId.get('attribute.body')?.enableRemove).toBe(true);
|
||||
expect(byId.get('log:body')?.enableRemove).toBe(false);
|
||||
expect(byId.get('attribute:body')?.enableRemove).toBe(true);
|
||||
});
|
||||
|
||||
it('applies the same distinct-column treatment to timestamp variants', () => {
|
||||
@@ -91,11 +91,11 @@ describe('useLogsTableColumns — selectColumns-order respected', () => {
|
||||
const byId = new Map(result.current.map((c) => [c.id, c]));
|
||||
expect(result.current.map((c) => c.id)).toStrictEqual([
|
||||
'state-indicator',
|
||||
'log.timestamp',
|
||||
'attribute.timestamp',
|
||||
'log:timestamp',
|
||||
'attribute:timestamp',
|
||||
]);
|
||||
expect(byId.get('log.timestamp')?.enableRemove).toBe(false);
|
||||
expect(byId.get('attribute.timestamp')?.enableRemove).toBe(true);
|
||||
expect(byId.get('log:timestamp')?.enableRemove).toBe(false);
|
||||
expect(byId.get('attribute:timestamp')?.enableRemove).toBe(true);
|
||||
});
|
||||
|
||||
it('skips the synthetic "id" field name', () => {
|
||||
@@ -127,10 +127,10 @@ describe('useLogsTableColumns — selectColumns-order respected', () => {
|
||||
|
||||
const byId = new Map(result.current.map((c) => [c.id, c]));
|
||||
// body + timestamp are locked from the table-X removal pathway.
|
||||
expect(byId.get('log.body')?.canBeHidden).toBe(false);
|
||||
expect(byId.get('log.body')?.enableRemove).toBe(false);
|
||||
expect(byId.get('log.timestamp')?.canBeHidden).toBe(false);
|
||||
expect(byId.get('log.timestamp')?.enableRemove).toBe(false);
|
||||
expect(byId.get('log:body')?.canBeHidden).toBe(false);
|
||||
expect(byId.get('log:body')?.enableRemove).toBe(false);
|
||||
expect(byId.get('log:timestamp')?.canBeHidden).toBe(false);
|
||||
expect(byId.get('log:timestamp')?.enableRemove).toBe(false);
|
||||
// User-added fields stay removable. User field has type='' so composite
|
||||
// collapses to bare name.
|
||||
expect(byId.get('user_field')?.enableRemove).toBe(true);
|
||||
|
||||
@@ -107,38 +107,14 @@ async function renderAndFocus(
|
||||
return editor;
|
||||
}
|
||||
|
||||
// Re-requests completions while waiting: typing and the async fetches can close the popup.
|
||||
function waitForRecents(
|
||||
assertLabels: (labels: string[]) => void,
|
||||
): Promise<void> {
|
||||
return waitFor(
|
||||
() => {
|
||||
const view = getEditorView();
|
||||
if (view && !isCompletionOpen()) {
|
||||
startCompletion(view);
|
||||
}
|
||||
assertLabels(getRecentLabels());
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
}
|
||||
|
||||
function openRecents(): Promise<void> {
|
||||
return waitForRecents((labels) => expect(labels.length).toBeGreaterThan(0));
|
||||
}
|
||||
|
||||
function waitForPopupElement(
|
||||
find: () => HTMLElement | null | undefined,
|
||||
): Promise<HTMLElement> {
|
||||
return waitFor(
|
||||
() => {
|
||||
const view = getEditorView();
|
||||
if (view && !isCompletionOpen()) {
|
||||
startCompletion(view);
|
||||
}
|
||||
const node = find();
|
||||
expect(node).toBeTruthy();
|
||||
return node as HTMLElement;
|
||||
expect(getRecentLabels().length).toBeGreaterThan(0);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
@@ -156,8 +132,11 @@ describe('QuerySearch recent searches', () => {
|
||||
await renderAndFocus();
|
||||
await openRecents();
|
||||
|
||||
await waitForRecents((labels) =>
|
||||
expect(labels).toStrictEqual([FRONTEND_FILTER]),
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(getRecentLabels()).toStrictEqual([FRONTEND_FILTER]);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
const view = getEditorView() as EditorView;
|
||||
@@ -173,8 +152,11 @@ describe('QuerySearch recent searches', () => {
|
||||
await openRecents();
|
||||
await userEvent.type(editor, 'status_code');
|
||||
|
||||
await waitForRecents((labels) =>
|
||||
expect(labels).toStrictEqual([STATUS_CODE_FILTER]),
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(getRecentLabels()).toStrictEqual([STATUS_CODE_FILTER]);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -188,8 +170,11 @@ describe('QuerySearch recent searches', () => {
|
||||
await renderAndFocus();
|
||||
await openRecents();
|
||||
|
||||
await waitForRecents((labels) =>
|
||||
expect(labels).toStrictEqual([FRONTEND_FILTER]),
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(getRecentLabels()).toStrictEqual([FRONTEND_FILTER]);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -202,8 +187,11 @@ describe('QuerySearch recent searches', () => {
|
||||
await openRecents();
|
||||
await userEvent.type(editor, FRONTEND_FILTER);
|
||||
|
||||
await waitForRecents((labels) =>
|
||||
expect(labels).toStrictEqual([supersetFilter]),
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(getRecentLabels()).toStrictEqual([supersetFilter]);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -218,8 +206,11 @@ describe('QuerySearch recent searches', () => {
|
||||
await renderAndFocus();
|
||||
await openRecents();
|
||||
|
||||
await waitForRecents((labels) =>
|
||||
expect(labels).toStrictEqual(expectedLabels),
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(getRecentLabels()).toStrictEqual(expectedLabels);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -230,13 +221,17 @@ describe('QuerySearch recent searches', () => {
|
||||
await renderAndFocus(onChange);
|
||||
await openRecents();
|
||||
|
||||
const option = await waitForPopupElement(() =>
|
||||
Array.from(
|
||||
document.querySelectorAll<HTMLElement>(COMPLETION_LABEL_SELECTOR),
|
||||
).find((element) => element.textContent === FRONTEND_FILTER),
|
||||
const option = await waitFor(
|
||||
() => {
|
||||
const node = Array.from(
|
||||
document.querySelectorAll<HTMLElement>(COMPLETION_LABEL_SELECTOR),
|
||||
).find((element) => element.textContent === FRONTEND_FILTER);
|
||||
expect(node).toBeDefined();
|
||||
return node as HTMLElement;
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
// fireEvent: userEvent's pointerdown blurs the editor, closing the popup before CM's mousedown apply.
|
||||
fireEvent.mouseDown(option);
|
||||
await userEvent.click(option);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
@@ -261,8 +256,13 @@ describe('QuerySearch recent searches', () => {
|
||||
await renderAndFocus();
|
||||
await openRecents();
|
||||
|
||||
const deleteButton = await waitForPopupElement(() =>
|
||||
document.querySelector<HTMLElement>(DELETE_BUTTON_SELECTOR),
|
||||
const deleteButton = await waitFor(
|
||||
() => {
|
||||
const button = document.querySelector(DELETE_BUTTON_SELECTOR);
|
||||
expect(button).toBeInTheDocument();
|
||||
return button as HTMLElement;
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
// fireEvent: the button preventDefaults pointerdown, which makes userEvent.click drop the mouse chain.
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
listRolesSuccessResponse,
|
||||
managedRoles,
|
||||
} from 'mocks-server/__mockdata__/roles';
|
||||
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
@@ -21,24 +18,17 @@ const ROLES_ENDPOINT = '*/api/v1/roles';
|
||||
const SA_KEYS_ENDPOINT = '*/api/v1/service_accounts/:id/keys';
|
||||
const SA_ENDPOINT = '*/api/v1/service_accounts/sa-1';
|
||||
const SA_DELETE_ENDPOINT = '*/api/v1/service_accounts/sa-1';
|
||||
const SA_ROLES_ENDPOINT = '*/api/v1/service_account_roles';
|
||||
const SA_ROLE_DELETE_ENDPOINT = '*/api/v1/service_account_roles/:id';
|
||||
const SA_ROLES_ENDPOINT = '*/api/v1/service_accounts/:id/roles';
|
||||
const SA_ROLE_DELETE_ENDPOINT = '*/api/v1/service_accounts/:id/roles/:rid';
|
||||
|
||||
const activeAccountResponse = {
|
||||
id: 'sa-1',
|
||||
name: 'CI Bot',
|
||||
email: 'ci-bot@signoz.io',
|
||||
roles: ['signoz-admin'],
|
||||
status: 'ACTIVE',
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
updatedAt: '2026-01-02T00:00:00Z',
|
||||
serviceAccountRoles: [
|
||||
{
|
||||
id: 'sar-admin-1',
|
||||
serviceAccountId: 'sa-1',
|
||||
roleId: managedRoles[0].id,
|
||||
role: { ...managedRoles[0], transactionGroups: [] },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function renderDrawer(
|
||||
@@ -68,13 +58,22 @@ function setupBaseHandlers(): void {
|
||||
rest.delete(SA_DELETE_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
rest.post(SA_ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
rest.get(SA_ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(201),
|
||||
ctx.json({ status: 'success', data: { id: 'sar-new' } }),
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: listRolesSuccessResponse.data.filter(
|
||||
(r) => r.name === 'signoz-admin',
|
||||
),
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) => res(ctx.status(204))),
|
||||
rest.post(SA_ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
listRolesSuccessResponse,
|
||||
managedRoles,
|
||||
} from 'mocks-server/__mockdata__/roles';
|
||||
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
@@ -13,33 +10,23 @@ const ROLES_ENDPOINT = '*/api/v1/roles';
|
||||
const SA_KEYS_ENDPOINT = '*/api/v1/service_accounts/:id/keys';
|
||||
const SA_ENDPOINT = '*/api/v1/service_accounts/sa-1';
|
||||
const SA_DELETE_ENDPOINT = '*/api/v1/service_accounts/sa-1';
|
||||
const SA_ROLES_ENDPOINT = '*/api/v1/service_account_roles';
|
||||
const SA_ROLE_DELETE_ENDPOINT = '*/api/v1/service_account_roles/:id';
|
||||
|
||||
const ADMIN_ASSIGNMENT_ID = 'sar-admin-1';
|
||||
const SA_ROLES_ENDPOINT = '*/api/v1/service_accounts/:id/roles';
|
||||
const SA_ROLE_DELETE_ENDPOINT = '*/api/v1/service_accounts/:id/roles/:rid';
|
||||
|
||||
const activeAccountResponse = {
|
||||
id: 'sa-1',
|
||||
name: 'CI Bot',
|
||||
email: 'ci-bot@signoz.io',
|
||||
roles: ['signoz-admin'],
|
||||
status: 'ACTIVE',
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
updatedAt: '2026-01-02T00:00:00Z',
|
||||
serviceAccountRoles: [
|
||||
{
|
||||
id: ADMIN_ASSIGNMENT_ID,
|
||||
serviceAccountId: 'sa-1',
|
||||
roleId: managedRoles[0].id,
|
||||
role: { ...managedRoles[0], transactionGroups: [] },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const deletedAccountResponse = {
|
||||
...activeAccountResponse,
|
||||
id: 'sa-2',
|
||||
status: 'DELETED',
|
||||
serviceAccountRoles: [],
|
||||
};
|
||||
|
||||
function renderDrawer(
|
||||
@@ -71,13 +58,22 @@ describe('ServiceAccountDrawer', () => {
|
||||
rest.delete(SA_DELETE_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
rest.post(SA_ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
rest.get(SA_ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(201),
|
||||
ctx.json({ status: 'success', data: { id: 'sar-new' } }),
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: listRolesSuccessResponse.data.filter(
|
||||
(r) => r.name === 'signoz-admin',
|
||||
),
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) => res(ctx.status(204))),
|
||||
rest.post(SA_ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
setupAuthzAdmin(),
|
||||
);
|
||||
});
|
||||
@@ -140,14 +136,11 @@ describe('ServiceAccountDrawer', () => {
|
||||
server.use(
|
||||
rest.post(SA_ROLES_ENDPOINT, async (req, res, ctx) => {
|
||||
roleSpy(await req.json());
|
||||
return res(
|
||||
ctx.status(201),
|
||||
ctx.json({ status: 'success', data: { id: 'sar-new' } }),
|
||||
);
|
||||
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
|
||||
}),
|
||||
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) => {
|
||||
deleteSpy();
|
||||
return res(ctx.status(204));
|
||||
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -166,8 +159,7 @@ describe('ServiceAccountDrawer', () => {
|
||||
await waitFor(() => {
|
||||
expect(roleSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
serviceAccountId: 'sa-1',
|
||||
roleId: '019c24aa-2248-7585-a129-4188b3473c27',
|
||||
id: '019c24aa-2248-7585-a129-4188b3473c27',
|
||||
}),
|
||||
);
|
||||
expect(deleteSpy).not.toHaveBeenCalled();
|
||||
@@ -182,14 +174,11 @@ describe('ServiceAccountDrawer', () => {
|
||||
server.use(
|
||||
rest.post(SA_ROLES_ENDPOINT, async (req, res, ctx) => {
|
||||
roleSpy(await req.json());
|
||||
return res(
|
||||
ctx.status(201),
|
||||
ctx.json({ status: 'success', data: { id: 'sar-new' } }),
|
||||
);
|
||||
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
|
||||
}),
|
||||
rest.delete(SA_ROLE_DELETE_ENDPOINT, (req, res, ctx) => {
|
||||
deleteSpy(req.params.id);
|
||||
return res(ctx.status(204));
|
||||
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) => {
|
||||
deleteSpy();
|
||||
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -209,7 +198,7 @@ describe('ServiceAccountDrawer', () => {
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteSpy).toHaveBeenCalledWith(ADMIN_ASSIGNMENT_ID);
|
||||
expect(deleteSpy).toHaveBeenCalled();
|
||||
expect(roleSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -257,6 +246,9 @@ describe('ServiceAccountDrawer', () => {
|
||||
rest.get('*/api/v1/service_accounts/sa-2/keys', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ data: [] })),
|
||||
),
|
||||
rest.get('*/api/v1/service_accounts/sa-2/roles', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ data: [] })),
|
||||
),
|
||||
);
|
||||
|
||||
renderDrawer({ account: 'sa-2' });
|
||||
@@ -320,13 +312,22 @@ describe('ServiceAccountDrawer – save-error UX', () => {
|
||||
rest.delete(SA_DELETE_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
rest.post(SA_ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
rest.get(SA_ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(201),
|
||||
ctx.json({ status: 'success', data: { id: 'sar-new' } }),
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: listRolesSuccessResponse.data.filter(
|
||||
(r) => r.name === 'signoz-admin',
|
||||
),
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) => res(ctx.status(204))),
|
||||
rest.post(SA_ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
setupAuthzAdmin(),
|
||||
);
|
||||
});
|
||||
@@ -409,17 +410,14 @@ describe('ServiceAccountDrawer – save-error UX', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
let roleAddCallCount = 0;
|
||||
|
||||
// First call → 429, second call → 201
|
||||
// First call → 429, second call → 200
|
||||
server.use(
|
||||
rest.post(SA_ROLES_ENDPOINT, (_, res, ctx) => {
|
||||
roleAddCallCount += 1;
|
||||
if (roleAddCallCount === 1) {
|
||||
return res(ctx.status(429), ctx.json({ message: 'Too Many Requests' }));
|
||||
}
|
||||
return res(
|
||||
ctx.status(201),
|
||||
ctx.json({ status: 'success', data: { id: 'sar-new' } }),
|
||||
);
|
||||
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export enum LOCALSTORAGE {
|
||||
TRACES_LIST_COLUMNS = 'TRACES_LIST_COLUMNS',
|
||||
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
|
||||
LOGS_LIST_COLUMN_SIZING = 'LOGS_LIST_COLUMN_SIZING',
|
||||
LOG_DETAILS_V2 = 'LOG_DETAILS_V2',
|
||||
LOGGED_IN_USER_NAME = 'LOGGED_IN_USER_NAME',
|
||||
LOGGED_IN_USER_EMAIL = 'LOGGED_IN_USER_EMAIL',
|
||||
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import useUpdatedQuery from '../useResolveQuery';
|
||||
|
||||
const mockGetSubstituteVars = jest.fn();
|
||||
const mockDynamicVariables: unknown[] = [];
|
||||
|
||||
jest.mock('api/dashboard/substitute_vars', () => ({
|
||||
getSubstituteVars: (...args: unknown[]): unknown =>
|
||||
mockGetSubstituteVars(...args),
|
||||
}));
|
||||
|
||||
jest.mock('api/v5/v5', () => ({
|
||||
prepareQueryRangePayloadV5: (): { queryPayload: unknown } => ({
|
||||
queryPayload: { start: 0, end: 1 },
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi',
|
||||
() => ({
|
||||
mapQueryDataFromApi: (): Query => ({ resolved: true }) as unknown as Query,
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('hooks/dashboard/useDashboardVariablesByType', () => ({
|
||||
useDashboardVariablesByType: (): unknown[] => mockDynamicVariables,
|
||||
}));
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: (): unknown => ({
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
}),
|
||||
}));
|
||||
|
||||
const QUERY = { builder: { queryData: [] } } as unknown as Query;
|
||||
|
||||
const WIDGET_CONFIG = {
|
||||
query: QUERY,
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
timePreferance: 'GLOBAL_TIME' as const,
|
||||
};
|
||||
|
||||
describe('useResolveQuery', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockDynamicVariables.length = 0;
|
||||
});
|
||||
|
||||
it('skips the substitute_vars round-trip when there are no variables', async () => {
|
||||
const { result } = renderHook(() => useUpdatedQuery(), {
|
||||
wrapper: MockQueryClientProvider,
|
||||
});
|
||||
|
||||
const resolved = await result.current.getUpdatedQuery({
|
||||
widgetConfig: WIDGET_CONFIG,
|
||||
});
|
||||
|
||||
expect(mockGetSubstituteVars).not.toHaveBeenCalled();
|
||||
expect(resolved).toBe(QUERY);
|
||||
});
|
||||
|
||||
it('resolves through substitute_vars when the dashboard has variables', async () => {
|
||||
mockGetSubstituteVars.mockResolvedValue({
|
||||
httpStatusCode: 200,
|
||||
data: { compositeQuery: {} },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUpdatedQuery(), {
|
||||
wrapper: MockQueryClientProvider,
|
||||
});
|
||||
|
||||
const resolved = await result.current.getUpdatedQuery({
|
||||
widgetConfig: WIDGET_CONFIG,
|
||||
dashboardData: {
|
||||
data: {
|
||||
variables: {
|
||||
env: { name: 'env', selectedValue: 'prod' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockGetSubstituteVars).toHaveBeenCalledTimes(1);
|
||||
expect(resolved).toStrictEqual({ resolved: true });
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,6 @@ import { useCallback } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { getSubstituteVars } from 'api/dashboard/substitute_vars';
|
||||
import { prepareQueryRangePayloadV5 } from 'api/v5/v5';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -47,21 +46,13 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
|
||||
widgetConfig,
|
||||
dashboardData,
|
||||
}: UseUpdatedQueryOptions): Promise<Query> => {
|
||||
const variables = getDashboardVariables(dashboardData?.data?.variables);
|
||||
|
||||
// `/substitute_vars` only rewrites `$variable` references, so on surfaces with no
|
||||
// dashboard behind them (APM, Celery, API monitoring) the round-trip is a no-op.
|
||||
if (isEmpty(variables) && isEmpty(dashboardDynamicVariables)) {
|
||||
return widgetConfig.query;
|
||||
}
|
||||
|
||||
// Prepare query payload with resolved variables
|
||||
const { queryPayload } = prepareQueryRangePayloadV5({
|
||||
query: widgetConfig.query,
|
||||
graphType: getGraphType(widgetConfig.panelTypes),
|
||||
selectedTime: widgetConfig.timePreferance,
|
||||
globalSelectedInterval,
|
||||
variables,
|
||||
variables: getDashboardVariables(dashboardData?.data?.variables),
|
||||
originalGraphType: widgetConfig.panelTypes,
|
||||
dynamicVariables: dashboardDynamicVariables,
|
||||
});
|
||||
|
||||
@@ -51,5 +51,4 @@
|
||||
gap: var(--spacing-3);
|
||||
max-width: 500px;
|
||||
padding: 24px;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ import { useInfraMonitoringFontSize } from './useInfraMonitoringTablePreferences
|
||||
import styles from './K8sExpandedRow.module.scss';
|
||||
import { buildExpressionFromGroupMeta } from './utils';
|
||||
import { logInfraColumnSortedEvent } from 'container/InfraMonitoringK8sV2/Base/events';
|
||||
import { getUnstableCurrentSearchParams } from 'utils/getUnstableCurrentSearchParams';
|
||||
import { getUnstableCurrentSearchParams } from 'container/TopNav/DateTimeSelectionV2/utils/getUnstableCurrentSearchParams';
|
||||
import { QueryParams } from 'constants/query';
|
||||
|
||||
const EXPANDED_ROW_LIMIT = 10;
|
||||
|
||||
@@ -9,11 +9,7 @@ function Overview(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className={styles.overview} data-testid="llm-observability-overview">
|
||||
<DashboardContainer
|
||||
dashboard={dashboard}
|
||||
refetch={refetch}
|
||||
canEditDashboardOverride={false}
|
||||
/>
|
||||
<DashboardContainer dashboard={dashboard} refetch={refetch} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "llm-observability-overview",
|
||||
"orgId": "",
|
||||
"locked": false,
|
||||
"locked": true,
|
||||
"name": "AI Observability Overview",
|
||||
"schemaVersion": "v6",
|
||||
"source": "system",
|
||||
@@ -1146,4 +1146,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +275,7 @@ function LiveLogsContainer({
|
||||
onClose={(): void => setIsFieldsSelectorOpen(false)}
|
||||
signal={DataSource.LOGS}
|
||||
requiredFields={LOGS_REQUIRED_COLUMNS}
|
||||
allowCustomFields
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2806,8 +2806,8 @@ export const hostWidgetInfo = [
|
||||
'The 1m, 5m and 15m load averages together; 1m above 15m means load is building.',
|
||||
},
|
||||
{
|
||||
title: 'Network usage',
|
||||
yAxisUnit: 'binBps',
|
||||
title: 'Network usage (bytes)',
|
||||
yAxisUnit: 'bytes',
|
||||
docPath: '/infrastructure-monitoring/host-monitoring/#network-usage-bytes',
|
||||
description:
|
||||
'Throughput in bytes/s per interface and direction, to spot NICs nearing rated bandwidth.',
|
||||
@@ -2841,8 +2841,8 @@ export const hostWidgetInfo = [
|
||||
'Active connection counts per protocol and state (ESTABLISHED, TIME_WAIT, SYN_RECV) to spot leaks and churn.',
|
||||
},
|
||||
{
|
||||
title: 'System disk IO',
|
||||
yAxisUnit: 'binBps',
|
||||
title: 'System disk io (bytes transferred)',
|
||||
yAxisUnit: 'bytes',
|
||||
docPath: '/infrastructure-monitoring/host-monitoring/#system-disk-io-bytes',
|
||||
description:
|
||||
'Disk throughput in bytes/s per device and direction, tracking heavy file I/O or database flushes.',
|
||||
|
||||
@@ -1,21 +1,4 @@
|
||||
.overview-container {
|
||||
.data-viewer {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.pretty-view__search-wrapper {
|
||||
background: var(--l2-background);
|
||||
}
|
||||
|
||||
.pretty-view__search-input {
|
||||
background: var(--l2-background) !important;
|
||||
}
|
||||
|
||||
.log-body-value {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.tag {
|
||||
border-radius: 20px;
|
||||
border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent);
|
||||
|
||||
@@ -13,28 +13,15 @@ import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { ChevronDown, ChevronRight, Search } from '@signozhq/icons';
|
||||
import { isLogDetailsV2 } from 'components/LogDetail/constants';
|
||||
import { DataViewer } from 'periscope/components/DataViewer';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { ActionItemProps } from './ActionItem';
|
||||
import { useLogAttributeActions } from './hooks/useLogAttributeActions';
|
||||
import TableView from './TableView';
|
||||
import {
|
||||
aggregateAttributesResourcesToObject,
|
||||
buildPrettyViewData,
|
||||
getBodyDisplayString,
|
||||
getSanitizedLogBody,
|
||||
removeEscapeCharacters,
|
||||
} from './utils';
|
||||
import { getBodyDisplayString, removeEscapeCharacters } from './utils';
|
||||
|
||||
import './Overview.styles.scss';
|
||||
|
||||
// Skip body sanitization above this size. sanitization is expensive and fails
|
||||
// for large bodies
|
||||
const MAX_BODY_SANITIZE_CHARS = 64 * 1024;
|
||||
|
||||
interface OverviewProps {
|
||||
logData: ILog;
|
||||
isListViewPanel?: boolean;
|
||||
@@ -64,52 +51,6 @@ function Overview({
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const { actions, visibleActions } = useLogAttributeActions({
|
||||
handleChangeSelectedView,
|
||||
isListViewPanel,
|
||||
});
|
||||
|
||||
if (isLogDetailsV2) {
|
||||
const raw = aggregateAttributesResourcesToObject(logData);
|
||||
const prettyData = buildPrettyViewData(raw);
|
||||
return (
|
||||
<div className="overview-container">
|
||||
<DataViewer
|
||||
data={prettyData}
|
||||
drawerKey="logs-details"
|
||||
fontSize={13}
|
||||
prettyViewProps={{
|
||||
actions,
|
||||
visibleActions,
|
||||
renderLeafValue: (value, keyPath): ReactNode | undefined => {
|
||||
// Sanitize (unescape + ANSI→color) string values under `body`.
|
||||
// Skip huge ones (render raw, still safe) to avoid the sanitize
|
||||
// choke;
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
keyPath[keyPath.length - 1] !== 'body' ||
|
||||
value.length > MAX_BODY_SANITIZE_CHARS
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
<span
|
||||
className="log-body-value"
|
||||
// Safe: getSanitizedLogBody runs the value through dompurify.
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: getSanitizedLogBody(value, { shouldEscapeHtml: true }),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}}
|
||||
jsonString={JSON.stringify(raw, null, 2)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const options: EditorProps['options'] = {
|
||||
automaticLayout: true,
|
||||
readOnly: true,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
export enum LogAttributeBucket {
|
||||
ATTRIBUTES = 'attributes',
|
||||
RESOURCES = 'resource',
|
||||
SCOPE = 'scope',
|
||||
}
|
||||
|
||||
export enum LogDetailsAction {
|
||||
COPY = 'copy',
|
||||
FILTER_IN = 'filter-in',
|
||||
FILTER_OUT = 'filter-out',
|
||||
GROUP_BY = 'group-by',
|
||||
REPLACE_FILTER = 'replace-filter',
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { CircleMinus, CirclePlus, Layers, RefreshCw } from '@signozhq/icons';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import {
|
||||
FieldContext,
|
||||
PrettyViewAction,
|
||||
VisibleActionsConfig,
|
||||
} from 'periscope/components/PrettyView/PrettyView';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import { LogDetailsAction } from '../constants';
|
||||
import {
|
||||
buildLogFilterTarget,
|
||||
getFilterQueryData,
|
||||
getGroupByQueryData,
|
||||
getReplaceFilterQueryData,
|
||||
} from '../logAttributeActions.utils';
|
||||
|
||||
interface UseLogAttributeActionsParams {
|
||||
handleChangeSelectedView?: ChangeViewFunctionType;
|
||||
isListViewPanel?: boolean;
|
||||
}
|
||||
|
||||
interface UseLogAttributeActionsResult {
|
||||
actions: PrettyViewAction[];
|
||||
visibleActions: VisibleActionsConfig;
|
||||
}
|
||||
|
||||
const COPY_ONLY_ACTIONS = [LogDetailsAction.COPY];
|
||||
const ALL_LEAF_ACTIONS = [
|
||||
LogDetailsAction.COPY,
|
||||
LogDetailsAction.FILTER_IN,
|
||||
LogDetailsAction.FILTER_OUT,
|
||||
LogDetailsAction.GROUP_BY,
|
||||
LogDetailsAction.REPLACE_FILTER,
|
||||
];
|
||||
|
||||
/**
|
||||
* PrettyView filter/group-by/replace actions for the log-details drawer (keys mapped via
|
||||
* buildLogFilterTarget). Also owns `visibleActions` (leaf/nested + list-panel copy-only).
|
||||
*/
|
||||
export function useLogAttributeActions({
|
||||
handleChangeSelectedView,
|
||||
isListViewPanel = false,
|
||||
}: UseLogAttributeActionsParams): UseLogAttributeActionsResult {
|
||||
const { pathname } = useLocation();
|
||||
const { stagedQuery, updateQueriesData } = useQueryBuilder();
|
||||
const { featureFlags } = useAppContext();
|
||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||
|
||||
const isBodyJsonQueryEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_JSON_BODY)
|
||||
?.active || false;
|
||||
|
||||
const isOldExplorerOrLive =
|
||||
pathname === ROUTES.OLD_LOGS_EXPLORER || pathname === ROUTES.LIVE_LOGS;
|
||||
|
||||
const filterFor = useCallback(
|
||||
(context: FieldContext, isFilterIn: boolean): void => {
|
||||
if (!stagedQuery) {
|
||||
return;
|
||||
}
|
||||
const target = buildLogFilterTarget(
|
||||
context.fieldKeyPath,
|
||||
context.fieldValue,
|
||||
isBodyJsonQueryEnabled,
|
||||
);
|
||||
const operator = isFilterIn
|
||||
? target.filterInOperator
|
||||
: target.filterOutOperator;
|
||||
|
||||
const updatedQuery = updateQueriesData(
|
||||
stagedQuery,
|
||||
'queryData',
|
||||
(item, index) =>
|
||||
index === 0
|
||||
? getFilterQueryData(item, target, context.fieldValue, operator)
|
||||
: item,
|
||||
);
|
||||
|
||||
const queryData: ICurrentQueryData = {
|
||||
name: viewName,
|
||||
id: updatedQuery.id,
|
||||
query: updatedQuery,
|
||||
};
|
||||
handleChangeSelectedView?.(ExplorerViews.LIST, queryData);
|
||||
},
|
||||
[
|
||||
stagedQuery,
|
||||
isBodyJsonQueryEnabled,
|
||||
updateQueriesData,
|
||||
viewName,
|
||||
handleChangeSelectedView,
|
||||
],
|
||||
);
|
||||
|
||||
const groupBy = useCallback(
|
||||
(context: FieldContext): void => {
|
||||
if (!stagedQuery) {
|
||||
return;
|
||||
}
|
||||
const target = buildLogFilterTarget(
|
||||
context.fieldKeyPath,
|
||||
context.fieldValue,
|
||||
isBodyJsonQueryEnabled,
|
||||
);
|
||||
if (!target.groupBySupported || !target.groupByKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedQuery = updateQueriesData(
|
||||
stagedQuery,
|
||||
'queryData',
|
||||
(item, index) => (index === 0 ? getGroupByQueryData(item, target) : item),
|
||||
);
|
||||
|
||||
const queryData: ICurrentQueryData = {
|
||||
name: viewName,
|
||||
id: updatedQuery.id,
|
||||
query: updatedQuery,
|
||||
};
|
||||
handleChangeSelectedView?.(ExplorerViews.TIMESERIES, queryData);
|
||||
},
|
||||
[
|
||||
stagedQuery,
|
||||
isBodyJsonQueryEnabled,
|
||||
updateQueriesData,
|
||||
viewName,
|
||||
handleChangeSelectedView,
|
||||
],
|
||||
);
|
||||
|
||||
const replaceFilter = useCallback(
|
||||
(context: FieldContext): void => {
|
||||
if (!stagedQuery) {
|
||||
return;
|
||||
}
|
||||
const target = buildLogFilterTarget(
|
||||
context.fieldKeyPath,
|
||||
context.fieldValue,
|
||||
isBodyJsonQueryEnabled,
|
||||
);
|
||||
|
||||
const updatedQuery = updateQueriesData(
|
||||
stagedQuery,
|
||||
'queryData',
|
||||
(item, index) =>
|
||||
index === 0
|
||||
? getReplaceFilterQueryData(item, target, context.fieldValue)
|
||||
: item,
|
||||
);
|
||||
|
||||
const queryData: ICurrentQueryData = {
|
||||
name: viewName,
|
||||
id: updatedQuery.id,
|
||||
query: updatedQuery,
|
||||
};
|
||||
handleChangeSelectedView?.(ExplorerViews.LIST, queryData);
|
||||
},
|
||||
[
|
||||
stagedQuery,
|
||||
isBodyJsonQueryEnabled,
|
||||
updateQueriesData,
|
||||
viewName,
|
||||
handleChangeSelectedView,
|
||||
],
|
||||
);
|
||||
|
||||
const actions: PrettyViewAction[] = useMemo(() => {
|
||||
const isRestricted = (fieldKeyPath: (string | number)[]): boolean =>
|
||||
buildLogFilterTarget(fieldKeyPath, undefined, isBodyJsonQueryEnabled)
|
||||
.isRestricted;
|
||||
|
||||
return [
|
||||
{
|
||||
key: LogDetailsAction.FILTER_IN,
|
||||
label: 'Filter for value',
|
||||
icon: <CirclePlus size={12} />,
|
||||
onClick: (context): void => filterFor(context, true),
|
||||
shouldHide: (_key, fieldKeyPath): boolean => isRestricted(fieldKeyPath),
|
||||
},
|
||||
{
|
||||
key: LogDetailsAction.FILTER_OUT,
|
||||
label: 'Filter out value',
|
||||
icon: <CircleMinus size={12} />,
|
||||
onClick: (context): void => filterFor(context, false),
|
||||
shouldHide: (_key, fieldKeyPath): boolean => isRestricted(fieldKeyPath),
|
||||
},
|
||||
{
|
||||
key: LogDetailsAction.GROUP_BY,
|
||||
label: 'Group by field',
|
||||
icon: <Layers size={12} />,
|
||||
onClick: groupBy,
|
||||
shouldHide: (_key, fieldKeyPath): boolean =>
|
||||
!buildLogFilterTarget(fieldKeyPath, undefined, isBodyJsonQueryEnabled)
|
||||
.groupBySupported || isOldExplorerOrLive,
|
||||
},
|
||||
{
|
||||
key: LogDetailsAction.REPLACE_FILTER,
|
||||
label: 'Replace filters with this value',
|
||||
icon: <RefreshCw size={12} />,
|
||||
onClick: replaceFilter,
|
||||
shouldHide: (_key, fieldKeyPath): boolean =>
|
||||
isRestricted(fieldKeyPath) || isOldExplorerOrLive,
|
||||
},
|
||||
];
|
||||
}, [
|
||||
filterFor,
|
||||
groupBy,
|
||||
replaceFilter,
|
||||
isBodyJsonQueryEnabled,
|
||||
isOldExplorerOrLive,
|
||||
]);
|
||||
|
||||
const visibleActions = useMemo<VisibleActionsConfig>(
|
||||
() => ({
|
||||
leaf: isListViewPanel ? COPY_ONLY_ACTIONS : ALL_LEAF_ACTIONS,
|
||||
nested: COPY_ONLY_ACTIONS,
|
||||
}),
|
||||
[isListViewPanel],
|
||||
);
|
||||
|
||||
return { actions, visibleActions };
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
|
||||
import {
|
||||
buildLogFilterTarget,
|
||||
toTypedFilterValue,
|
||||
} from './logAttributeActions.utils';
|
||||
|
||||
describe('buildLogFilterTarget', () => {
|
||||
describe('attributes / resources / scope / top-level scalars', () => {
|
||||
it('maps a top-level scalar to its bare key with =/!=, groupable', () => {
|
||||
const t = buildLogFilterTarget(['severity_text'], 'ERROR', true);
|
||||
expect(t).toMatchObject({
|
||||
fieldKey: 'severity_text',
|
||||
filterInOperator: '=',
|
||||
filterOutOperator: '!=',
|
||||
groupBySupported: true,
|
||||
groupByKey: 'severity_text',
|
||||
isRestricted: false,
|
||||
});
|
||||
expect(t.metricsType).toBeUndefined();
|
||||
});
|
||||
|
||||
it('strips the `attributes` root to a bare dotted key + Tag type', () => {
|
||||
expect(
|
||||
buildLogFilterTarget(['attributes', 'http.method'], 'GET', true),
|
||||
).toMatchObject({
|
||||
fieldKey: 'http.method',
|
||||
filterInOperator: '=',
|
||||
metricsType: MetricsType.Tag,
|
||||
groupBySupported: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('maps `resources` with Resource type', () => {
|
||||
expect(
|
||||
buildLogFilterTarget(['resource', 'service.name'], 'api', true),
|
||||
).toMatchObject({
|
||||
fieldKey: 'service.name',
|
||||
metricsType: MetricsType.Resource,
|
||||
});
|
||||
});
|
||||
|
||||
it('maps `scope` with Scope type', () => {
|
||||
expect(
|
||||
buildLogFilterTarget(['scope', 'name'], 'my-scope', true),
|
||||
).toMatchObject({ fieldKey: 'name', metricsType: MetricsType.Scope });
|
||||
});
|
||||
|
||||
it('offers group-by for attributes now', () => {
|
||||
expect(
|
||||
buildLogFilterTarget(['attributes', 'k'], 'v', true).groupBySupported,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested attribute values (parsed JSON)', () => {
|
||||
it('marks a sub-field of a parsed attribute copy-only (restricted, no group-by)', () => {
|
||||
const t = buildLogFilterTarget(['attributes', 'payload', 'x'], 1, true);
|
||||
expect(t.isRestricted).toBe(true);
|
||||
expect(t.groupBySupported).toBe(false);
|
||||
expect(t.groupByKey).toBeUndefined();
|
||||
});
|
||||
|
||||
it('leaves a top-level attribute (depth 2) filterable', () => {
|
||||
const t = buildLogFilterTarget(['attributes', 'payload'], 'v', true);
|
||||
expect(t.isRestricted).toBe(false);
|
||||
expect(t.groupBySupported).toBe(true);
|
||||
});
|
||||
|
||||
it('does not restrict nested resource/scope values', () => {
|
||||
expect(
|
||||
buildLogFilterTarget(['resource', 'k8s', 'pod'], 'p', true).isRestricted,
|
||||
).toBe(false);
|
||||
expect(
|
||||
buildLogFilterTarget(['scope', 'a', 'b'], 'v', true).isRestricted,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restricted fields (timestamp / id)', () => {
|
||||
it.each(['timestamp', 'id'])(
|
||||
'marks %s restricted with no group-by',
|
||||
(key) => {
|
||||
const t = buildLogFilterTarget([key], 'v', true);
|
||||
expect(t.isRestricted).toBe(true);
|
||||
expect(t.groupBySupported).toBe(false);
|
||||
expect(t.groupByKey).toBeUndefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('group-by-restricted fields (trace_id)', () => {
|
||||
it('allows filtering but not group-by on top-level trace_id', () => {
|
||||
const t = buildLogFilterTarget(['trace_id'], 'abc123', true);
|
||||
expect(t.isRestricted).toBe(false);
|
||||
expect(t.filterInOperator).toBe('=');
|
||||
expect(t.groupBySupported).toBe(false);
|
||||
expect(t.groupByKey).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['resource', ['resource', 'trace_id']],
|
||||
['attributes', ['attributes', 'trace_id']],
|
||||
])(
|
||||
'blocks group-by on a %s field named trace_id, keeping filter',
|
||||
(_bucket, path) => {
|
||||
const t = buildLogFilterTarget(path as string[], 'abc123', true);
|
||||
expect(t.isRestricted).toBe(false);
|
||||
expect(t.filterInOperator).toBe('=');
|
||||
expect(t.groupBySupported).toBe(false);
|
||||
expect(t.groupByKey).toBeUndefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('body scalars', () => {
|
||||
it('maps a top-level body scalar to body.<key> with =/!=, groupable when json body on', () => {
|
||||
const t = buildLogFilterTarget(['body', 'message'], 'hello', true);
|
||||
expect(t).toMatchObject({
|
||||
fieldKey: 'body.message',
|
||||
filterInOperator: '=',
|
||||
filterOutOperator: '!=',
|
||||
groupBySupported: true,
|
||||
groupByKey: 'body.message',
|
||||
isRestricted: false,
|
||||
});
|
||||
expect(t.dataType).toBeDefined();
|
||||
expect(t.metricsType).toBeUndefined();
|
||||
});
|
||||
|
||||
it('maps a nested body scalar to a dotted body key', () => {
|
||||
expect(buildLogFilterTarget(['body', 'a', 'b'], 'x', true)).toMatchObject({
|
||||
fieldKey: 'body.a.b',
|
||||
groupBySupported: true,
|
||||
groupByKey: 'body.a.b',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not offer group-by when USE_JSON_BODY is off', () => {
|
||||
const t = buildLogFilterTarget(['body', 'message'], 'hello', false);
|
||||
expect(t.groupBySupported).toBe(false);
|
||||
expect(t.groupByKey).toBeUndefined();
|
||||
expect(t.fieldKey).toBe('body.message');
|
||||
});
|
||||
|
||||
it('filters the whole `body` field when body is an unparsed string leaf', () => {
|
||||
expect(buildLogFilterTarget(['body'], 'raw text', true)).toMatchObject({
|
||||
fieldKey: 'body',
|
||||
filterInOperator: '=',
|
||||
groupBySupported: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('restricts a body leaf named `timestamp` (no filter / group-by)', () => {
|
||||
const t = buildLogFilterTarget(['body', 'timestamp'], '2026-01-01', true);
|
||||
expect(t.fieldKey).toBe('body.timestamp');
|
||||
expect(t.isRestricted).toBe(true);
|
||||
expect(t.groupBySupported).toBe(false);
|
||||
expect(t.groupByKey).toBeUndefined();
|
||||
});
|
||||
|
||||
it('restricts a nested body leaf named `timestamp`', () => {
|
||||
const t = buildLogFilterTarget(['body', 'obj', 'timestamp'], 'x', true);
|
||||
expect(t.isRestricted).toBe(true);
|
||||
expect(t.groupBySupported).toBe(false);
|
||||
});
|
||||
|
||||
it('restricts a body leaf named `id` (uses RESTRICTED_SELECTED_FIELDS)', () => {
|
||||
expect(buildLogFilterTarget(['body', 'id'], 'abc', true).isRestricted).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not restrict an ordinary body leaf', () => {
|
||||
expect(
|
||||
buildLogFilterTarget(['body', 'message'], 'hello', true).isRestricted,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('body arrays', () => {
|
||||
it('uses has()/!has() on the array key for a primitive array element', () => {
|
||||
const t = buildLogFilterTarget(['body', 'tags', 0], 'urgent', true);
|
||||
expect(t).toMatchObject({
|
||||
fieldKey: 'body.tags',
|
||||
filterInOperator: 'has',
|
||||
groupBySupported: false,
|
||||
});
|
||||
expect(t.filterOutOperator).toContain('has');
|
||||
expect(t.filterOutOperator).not.toBe('has');
|
||||
});
|
||||
|
||||
it('collapses deep array-element paths to a []-marked has() key', () => {
|
||||
expect(
|
||||
buildLogFilterTarget(
|
||||
['body', 'config', 'features', 1, 'items', 0, 'variants', 2],
|
||||
'ballpen',
|
||||
true,
|
||||
),
|
||||
).toMatchObject({
|
||||
fieldKey: 'body.config.features[].items[].variants',
|
||||
filterInOperator: 'has',
|
||||
});
|
||||
});
|
||||
|
||||
it('maps a field nested inside an array element with =/!= and a []-marked key, no group-by', () => {
|
||||
expect(
|
||||
buildLogFilterTarget(['body', 'items', 2, 'sku'], 'ABC', true),
|
||||
).toMatchObject({
|
||||
fieldKey: 'body.items[].sku',
|
||||
filterInOperator: '=',
|
||||
filterOutOperator: '!=',
|
||||
groupBySupported: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the [*] string-body marker for an array element when USE_JSON_BODY is off', () => {
|
||||
expect(
|
||||
buildLogFilterTarget(['body', 'tags', 0], 'urgent', false),
|
||||
).toMatchObject({ fieldKey: 'body.tags[*]', filterInOperator: 'has' });
|
||||
});
|
||||
|
||||
it('uses [*] for a field nested inside an array element when USE_JSON_BODY is off', () => {
|
||||
expect(
|
||||
buildLogFilterTarget(['body', 'items', 2, 'sku'], 'ABC', false),
|
||||
).toMatchObject({ fieldKey: 'body.items[*].sku', filterInOperator: '=' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toTypedFilterValue', () => {
|
||||
const run = (value: unknown): unknown => toTypedFilterValue(value);
|
||||
|
||||
it('keeps numbers/booleans as their JS type (so the expression stays unquoted)', () => {
|
||||
expect(run(848)).toBe(848);
|
||||
expect(typeof run(848)).toBe('number');
|
||||
expect(run(1.1)).toBe(1.1);
|
||||
expect(run(true)).toBe(true);
|
||||
expect(typeof run(true)).toBe('boolean');
|
||||
});
|
||||
|
||||
it('passes strings through unchanged (no numeric inference)', () => {
|
||||
expect(run('unknown_service')).toBe('unknown_service');
|
||||
expect(typeof run('12345')).toBe('string');
|
||||
});
|
||||
});
|
||||
@@ -1,241 +0,0 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import {
|
||||
negateOperator,
|
||||
OPERATORS,
|
||||
QUERY_BUILDER_FUNCTIONS,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { OPERATORS as QUERY_BUILDER_OPERATORS } from 'constants/queryBuilder';
|
||||
import {
|
||||
RESTRICTED_GROUP_BY_FIELDS,
|
||||
RESTRICTED_SELECTED_FIELDS,
|
||||
} from 'container/LogsFilters/config';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { LogAttributeBucket } from './constants';
|
||||
import { generateFieldKeyForArray, getDataTypes } from './utils';
|
||||
|
||||
export const toTypedFilterValue = (value: unknown): string =>
|
||||
typeof value === 'number' || typeof value === 'boolean'
|
||||
? (value as unknown as string)
|
||||
: String(value);
|
||||
|
||||
export interface LogFilterTarget {
|
||||
fieldKey: string;
|
||||
filterInOperator: string;
|
||||
filterOutOperator: string;
|
||||
dataType?: DataTypes;
|
||||
metricsType?: MetricsType;
|
||||
groupBySupported: boolean;
|
||||
groupByKey?: string;
|
||||
isRestricted: boolean;
|
||||
}
|
||||
|
||||
// Collapse a body forward path into the query-builder key segment; array indices become
|
||||
// `[]` (json body on) or `[*]` (string body off — a distinct operator/search path).
|
||||
// ['items', 2, 'sku'] -> 'items[].sku' (json on) / 'items[*].sku' (off)
|
||||
// ['tags', 0] -> 'tags[]' (json on) / 'tags[*]' (off)
|
||||
const collapseBodyPath = (
|
||||
subpath: (string | number)[],
|
||||
isBodyJsonQueryEnabled: boolean,
|
||||
): string => {
|
||||
const arrayMarker = isBodyJsonQueryEnabled ? '[]' : '[*]';
|
||||
let out = '';
|
||||
subpath.forEach((seg) => {
|
||||
if (typeof seg === 'number') {
|
||||
out += arrayMarker;
|
||||
} else {
|
||||
out += out ? `.${seg}` : seg;
|
||||
}
|
||||
});
|
||||
return out;
|
||||
};
|
||||
|
||||
const metricsTypeForRoot = (root: string | number): MetricsType | undefined => {
|
||||
if (root === LogAttributeBucket.ATTRIBUTES) {
|
||||
return MetricsType.Tag;
|
||||
}
|
||||
if (root === LogAttributeBucket.RESOURCES) {
|
||||
return MetricsType.Resource;
|
||||
}
|
||||
if (root === LogAttributeBucket.SCOPE) {
|
||||
return MetricsType.Scope;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Map a PrettyView leaf (forward keyPath) to its query-builder filter/group-by target:
|
||||
* scalars → `=`/`!=`, body array elements → `has`/`!has`; group-by gated by
|
||||
* `groupBySupported`. Attribute/resource/scope carry a `metricsType` (Tag/Resource/Scope).
|
||||
*/
|
||||
export const buildLogFilterTarget = (
|
||||
fieldKeyPath: (string | number)[],
|
||||
value: unknown,
|
||||
isBodyJsonQueryEnabled: boolean,
|
||||
): LogFilterTarget => {
|
||||
const root = fieldKeyPath[0];
|
||||
|
||||
// Attributes / resources / scope / top-level scalars: bare dotted key, =/!=.
|
||||
if (root !== 'body') {
|
||||
const fieldKey =
|
||||
fieldKeyPath.length > 1 ? fieldKeyPath.slice(1).join('.') : String(root);
|
||||
// Temporarily removing filter/group-by support for nested attributes.
|
||||
// This will be removed once backend starts to support these actions.
|
||||
const isNestedAttributeValue =
|
||||
root === LogAttributeBucket.ATTRIBUTES && fieldKeyPath.length > 2;
|
||||
|
||||
const isRestricted =
|
||||
RESTRICTED_SELECTED_FIELDS.includes(fieldKey) || isNestedAttributeValue;
|
||||
|
||||
const groupBySupported =
|
||||
!isRestricted && !RESTRICTED_GROUP_BY_FIELDS.includes(fieldKey);
|
||||
return {
|
||||
fieldKey,
|
||||
filterInOperator: OPERATORS['='],
|
||||
filterOutOperator: OPERATORS['!='],
|
||||
dataType: getDataTypes(value),
|
||||
metricsType: metricsTypeForRoot(root),
|
||||
groupBySupported,
|
||||
groupByKey: groupBySupported ? fieldKey : undefined,
|
||||
isRestricted,
|
||||
};
|
||||
}
|
||||
|
||||
const subpath = fieldKeyPath.slice(1);
|
||||
|
||||
// Whole-body leaf (unparsed string body): filter on the `body` field itself.
|
||||
if (subpath.length === 0) {
|
||||
return {
|
||||
fieldKey: 'body',
|
||||
filterInOperator: OPERATORS['='],
|
||||
filterOutOperator: OPERATORS['!='],
|
||||
dataType: getDataTypes(value),
|
||||
groupBySupported: false,
|
||||
isRestricted: false,
|
||||
};
|
||||
}
|
||||
|
||||
const collapsed = collapseBodyPath(subpath, isBodyJsonQueryEnabled);
|
||||
const isArrayElement = typeof subpath[subpath.length - 1] === 'number';
|
||||
|
||||
if (isArrayElement) {
|
||||
// has(body.<array>, value): generateFieldKeyForArray strips the trailing value
|
||||
// segment + `[]` exactly as the old BodyTitleRenderer.filterHandler did.
|
||||
const fieldKey = generateFieldKeyForArray(
|
||||
`${collapsed}.${String(value)}`,
|
||||
getDataTypes(value),
|
||||
isBodyJsonQueryEnabled,
|
||||
);
|
||||
return {
|
||||
fieldKey,
|
||||
filterInOperator: QUERY_BUILDER_FUNCTIONS.HAS,
|
||||
filterOutOperator: negateOperator(QUERY_BUILDER_FUNCTIONS.HAS),
|
||||
dataType: getDataTypes([value]),
|
||||
groupBySupported: false,
|
||||
isRestricted: false,
|
||||
};
|
||||
}
|
||||
|
||||
const fieldKey = `body.${collapsed}`;
|
||||
// Restrict body leaves whose own key is a restricted field (e.g. a JSON body's own
|
||||
// `timestamp`/`id`/`date`) — hides filter / group-by, same as top-level fields.
|
||||
const leafKey = String(subpath[subpath.length - 1]);
|
||||
const isRestricted = RESTRICTED_SELECTED_FIELDS.includes(leafKey);
|
||||
// Group by only for plain body scalars (no array anywhere in the path) with json
|
||||
// body on — mirrors isGroupBySupported in the old BodyTitleRenderer.
|
||||
const groupBySupported =
|
||||
isBodyJsonQueryEnabled && !collapsed.includes('[]') && !isRestricted;
|
||||
return {
|
||||
fieldKey,
|
||||
filterInOperator: OPERATORS['='],
|
||||
filterOutOperator: OPERATORS['!='],
|
||||
dataType: getDataTypes(value),
|
||||
groupBySupported,
|
||||
groupByKey: groupBySupported ? fieldKey : undefined,
|
||||
isRestricted,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeDataType = (
|
||||
dataType: DataTypes | undefined,
|
||||
): DataTypes | undefined =>
|
||||
dataType && Object.values(DataTypes).includes(dataType) ? dataType : undefined;
|
||||
|
||||
// Append a filter item (filter-in / filter-out) to a query-data item. The autocomplete
|
||||
// key is fabricated locally (empty source list), so no click-time getAggregateKeys fetch.
|
||||
export const getFilterQueryData = (
|
||||
item: IBuilderQuery,
|
||||
target: LogFilterTarget,
|
||||
value: unknown,
|
||||
operator: string,
|
||||
): IBuilderQuery => {
|
||||
const filterKey = chooseAutocompleteFromCustomValue(
|
||||
[],
|
||||
target.fieldKey,
|
||||
target.dataType,
|
||||
target.metricsType,
|
||||
);
|
||||
return {
|
||||
...item,
|
||||
filters: {
|
||||
items: [
|
||||
...(item.filters?.items || []),
|
||||
{
|
||||
id: uuid(),
|
||||
key: filterKey,
|
||||
op: getOperatorValue(operator),
|
||||
value: toTypedFilterValue(value),
|
||||
},
|
||||
],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Append a group-by. Caller guards groupBySupported / groupByKey.
|
||||
export const getGroupByQueryData = (
|
||||
item: IBuilderQuery,
|
||||
target: LogFilterTarget,
|
||||
): IBuilderQuery => {
|
||||
const newGroupByItem: BaseAutocompleteData = {
|
||||
key: target.groupByKey || '',
|
||||
type: target.metricsType || '',
|
||||
dataType: normalizeDataType(target.dataType),
|
||||
};
|
||||
return { ...item, groupBy: [...(item.groupBy || []), newGroupByItem] };
|
||||
};
|
||||
|
||||
// Replace all filters with a single IN filter on this value.
|
||||
export const getReplaceFilterQueryData = (
|
||||
item: IBuilderQuery,
|
||||
target: LogFilterTarget,
|
||||
value: unknown,
|
||||
): IBuilderQuery => {
|
||||
const newFilterItem: BaseAutocompleteData = {
|
||||
key: target.fieldKey,
|
||||
type: target.metricsType || '',
|
||||
dataType: normalizeDataType(target.dataType),
|
||||
};
|
||||
return {
|
||||
...item,
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: '',
|
||||
key: newFilterItem,
|
||||
op: QUERY_BUILDER_OPERATORS.IN,
|
||||
value: [toTypedFilterValue(value)],
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
filter: { expression: '' },
|
||||
};
|
||||
};
|
||||
@@ -1,121 +1,12 @@
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import {
|
||||
aggregateAttributesResourcesToObject,
|
||||
buildPrettyViewData,
|
||||
flattenObject,
|
||||
getDataTypes,
|
||||
getSanitizedLogBody,
|
||||
parseJsonStringValue,
|
||||
recursiveParseJSON,
|
||||
} from './utils';
|
||||
|
||||
describe('parseJsonStringValue', () => {
|
||||
it('parses a JSON-object string into an object', () => {
|
||||
expect(parseJsonStringValue('{"a":1,"b":{"c":2}}')).toStrictEqual({
|
||||
a: 1,
|
||||
b: { c: 2 },
|
||||
});
|
||||
});
|
||||
|
||||
it('parses a JSON-array string into an array', () => {
|
||||
expect(parseJsonStringValue('[1,2,3]')).toStrictEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('returns a plain (non-JSON) string unchanged', () => {
|
||||
expect(parseJsonStringValue('plain log line')).toBe('plain log line');
|
||||
});
|
||||
|
||||
it('returns a string that is not object/array-looking unchanged', () => {
|
||||
expect(parseJsonStringValue('42')).toBe('42');
|
||||
});
|
||||
|
||||
it('returns an invalid JSON string unchanged', () => {
|
||||
expect(parseJsonStringValue('{not valid}')).toBe('{not valid}');
|
||||
});
|
||||
|
||||
it('returns an already-object value unchanged (same reference)', () => {
|
||||
const value = { message: 'hi', a: 1 };
|
||||
expect(parseJsonStringValue(value)).toBe(value);
|
||||
});
|
||||
|
||||
it('leaves a value larger than the 128KB parse guard as a string', () => {
|
||||
const huge = `{"x":"${'a'.repeat(130 * 1024)}"}`;
|
||||
expect(parseJsonStringValue(huge)).toBe(huge);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildPrettyViewData', () => {
|
||||
const baseRaw = {
|
||||
id: 'log-1',
|
||||
timestamp: 1234,
|
||||
body: 'hello',
|
||||
attributes: {},
|
||||
resource: {},
|
||||
scope: {},
|
||||
} as any;
|
||||
|
||||
it('parses a JSON-string body into a tree', () => {
|
||||
const result = buildPrettyViewData({ ...baseRaw, body: '{"a":1}' });
|
||||
expect(result.body).toStrictEqual({ a: 1 });
|
||||
});
|
||||
|
||||
it('parses attribute values that are JSON strings, leaves others as-is', () => {
|
||||
const result = buildPrettyViewData({
|
||||
...baseRaw,
|
||||
attributes: { payload: '{"x":1}', name: 'cart', count: 3 },
|
||||
});
|
||||
expect(result.attributes).toStrictEqual({
|
||||
payload: { x: 1 },
|
||||
name: 'cart',
|
||||
count: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('drops undefined fields so they do not render as empty rows', () => {
|
||||
const result = buildPrettyViewData({ ...baseRaw, trace_id: undefined });
|
||||
expect('trace_id' in result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregateAttributesResourcesToObject', () => {
|
||||
const mockLog = {
|
||||
id: 'log-1',
|
||||
timestamp: 1234,
|
||||
body: 'hello',
|
||||
severity_text: 'INFO',
|
||||
severity_number: 9,
|
||||
attributes_string: { 'http.method': 'GET' },
|
||||
attributes_int: { retries: 3 },
|
||||
resources_string: { 'service.name': 'cart' },
|
||||
scope_string: { lib: 'otel' },
|
||||
} as unknown as ILog;
|
||||
|
||||
it('merges attributes_/resources_/scope_ maps and keeps scalars + body', () => {
|
||||
const result = aggregateAttributesResourcesToObject(mockLog);
|
||||
|
||||
expect(result.attributes).toStrictEqual({
|
||||
'http.method': 'GET',
|
||||
retries: 3,
|
||||
});
|
||||
expect(result.resource).toStrictEqual({ 'service.name': 'cart' });
|
||||
expect(result.scope).toStrictEqual({ lib: 'otel' });
|
||||
expect(result.body).toBe('hello');
|
||||
expect(result.id).toBe('log-1');
|
||||
expect(result.severity_text).toBe('INFO');
|
||||
});
|
||||
|
||||
it('does not parse a JSON-string body (leaves it raw)', () => {
|
||||
const result = aggregateAttributesResourcesToObject({
|
||||
...mockLog,
|
||||
body: '{"a":1}',
|
||||
} as unknown as ILog);
|
||||
|
||||
expect(result.body).toBe('{"a":1}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('recursiveParseJSON', () => {
|
||||
it('should return an empty object if the input is not valid JSON', () => {
|
||||
const result = recursiveParseJSON('not valid JSON');
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import Convert from 'ansi-to-html';
|
||||
import type { DataNode } from 'antd/es/tree';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
@@ -263,11 +262,10 @@ export const filterKeyForField = (field: string): string => {
|
||||
return fieldAttribs?.newField || field;
|
||||
};
|
||||
|
||||
export const aggregateAttributesResourcesToObject = (
|
||||
logData: ILog,
|
||||
): ILogAggregateAttributesResources => {
|
||||
export const aggregateAttributesResourcesToString = (logData: ILog): string => {
|
||||
const outputJson: ILogAggregateAttributesResources = {
|
||||
body: logData.body,
|
||||
date: logData.date,
|
||||
id: logData.id,
|
||||
severityNumber: logData.severityNumber,
|
||||
severityText: logData.severityText,
|
||||
@@ -276,94 +274,29 @@ export const aggregateAttributesResourcesToObject = (
|
||||
traceFlags: logData.traceFlags,
|
||||
traceId: logData.traceId,
|
||||
attributes: {},
|
||||
resource: {},
|
||||
resources: {},
|
||||
scope: {},
|
||||
severity_text: logData.severity_text,
|
||||
severity_number: logData.severity_number,
|
||||
};
|
||||
|
||||
Object.keys(logData).forEach((key) => {
|
||||
if (key === 'date') {
|
||||
return;
|
||||
}
|
||||
if (key.startsWith('attributes_')) {
|
||||
outputJson.attributes = outputJson.attributes || {};
|
||||
Object.assign(outputJson.attributes, logData[key as keyof ILog]);
|
||||
} else if (key.startsWith('resources_')) {
|
||||
outputJson.resource = outputJson.resource || {};
|
||||
Object.assign(outputJson.resource, logData[key as keyof ILog]);
|
||||
outputJson.resources = outputJson.resources || {};
|
||||
Object.assign(outputJson.resources, logData[key as keyof ILog]);
|
||||
} else if (key.startsWith('scope_string')) {
|
||||
outputJson.scope = outputJson.scope || {};
|
||||
Object.assign(outputJson.scope, logData[key as keyof ILog]);
|
||||
} else {
|
||||
// @ts-expect-error dynamic top-level copy
|
||||
// @ts-expect-error
|
||||
outputJson[key] = logData[key as keyof ILog];
|
||||
}
|
||||
});
|
||||
|
||||
// Show `timestamp` first and `id` last in the details view.
|
||||
const { timestamp, id, ...rest } = outputJson;
|
||||
return { timestamp, ...rest, id };
|
||||
};
|
||||
|
||||
export const aggregateAttributesResourcesToString = (logData: ILog): string => {
|
||||
try {
|
||||
return JSON.stringify(aggregateAttributesResourcesToObject(logData), null, 2);
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const MAX_JSON_PARSE_BYTES = 128 * 1024;
|
||||
|
||||
// A JSON-encoded object/array string is parsed so DataViewer renders it as a tree
|
||||
// instead of one escaped string; non-JSON / plain-text values are returned unchanged.
|
||||
// Guarded against very large payloads.
|
||||
export const parseJsonStringValue = (value: unknown): unknown => {
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
const looksLikeJson = trimmed.startsWith('{') || trimmed.startsWith('[');
|
||||
if (!looksLikeJson || trimmed.length > MAX_JSON_PARSE_BYTES) {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
return parsed !== null && typeof parsed === 'object' ? parsed : value;
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
// Parse each attribute value that's a stringified JSON string into an object
|
||||
// Non-JSON values are left unchanged.
|
||||
const parseAttributeJsonValues = (
|
||||
attributes: Record<string, unknown>,
|
||||
): Record<string, unknown> => {
|
||||
const parsed: Record<string, unknown> = {};
|
||||
Object.keys(attributes).forEach((key) => {
|
||||
parsed[key] = parseJsonStringValue(attributes[key]);
|
||||
});
|
||||
return parsed;
|
||||
};
|
||||
|
||||
export const buildPrettyViewData = (
|
||||
raw: ILogAggregateAttributesResources,
|
||||
): Record<string, unknown> => {
|
||||
const prettyData: Record<string, unknown> = { ...raw };
|
||||
prettyData.body = parseJsonStringValue(raw.body);
|
||||
prettyData.attributes = parseAttributeJsonValues(raw.attributes);
|
||||
|
||||
// drop undefined fields so they don't render as empty rows
|
||||
Object.keys(prettyData).forEach((key) => {
|
||||
if (prettyData[key] === undefined) {
|
||||
delete prettyData[key];
|
||||
}
|
||||
});
|
||||
|
||||
return prettyData;
|
||||
return JSON.stringify(outputJson, null, 2);
|
||||
};
|
||||
|
||||
const isFloat = (num: number): boolean => num % 1 !== 0;
|
||||
|
||||
@@ -113,6 +113,7 @@ function LogsActionsContainer({
|
||||
onClose={(): void => setIsFieldsSelectorOpen(false)}
|
||||
signal={DataSource.LOGS}
|
||||
requiredFields={LOGS_REQUIRED_COLUMNS}
|
||||
allowCustomFields
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,6 @@ import { blue, red } from '@ant-design/colors';
|
||||
|
||||
export const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id'];
|
||||
|
||||
// Fields that can be filtered on but not grouped by in the log details view.
|
||||
export const RESTRICTED_GROUP_BY_FIELDS = ['body', 'trace_id'];
|
||||
|
||||
export const ICON_STYLE = {
|
||||
PLUS: { color: blue[5] },
|
||||
CLOSE: { color: red[5] },
|
||||
|
||||
@@ -124,9 +124,6 @@ function Application(): JSX.Element {
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
}),
|
||||
// the time range is part of the key, so without this every window change blanks the
|
||||
// operations list and the widgets below are rebuilt with an empty `operation in []`
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
const selectedTraceTags: string = JSON.stringify(
|
||||
|
||||
@@ -296,12 +296,12 @@ describe('useOptionsMenu', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// New order: [attribute.service.name, log.body, resource.service.name, log.timestamp]
|
||||
// New order: [attribute:service.name, log:body, resource:service.name, log:timestamp]
|
||||
result.current.config.addColumn?.onReorder([
|
||||
'attribute.service.name',
|
||||
'log.body',
|
||||
'resource.service.name',
|
||||
'log.timestamp',
|
||||
'attribute:service.name',
|
||||
'log:body',
|
||||
'resource:service.name',
|
||||
'log:timestamp',
|
||||
]);
|
||||
|
||||
expect(mockUpdateColumns).toHaveBeenCalledTimes(1);
|
||||
@@ -309,13 +309,13 @@ describe('useOptionsMenu', () => {
|
||||
expect(
|
||||
reordered.map(
|
||||
(c: { name: string; fieldContext: string }) =>
|
||||
`${c.fieldContext}.${c.name}`,
|
||||
`${c.fieldContext}:${c.name}`,
|
||||
),
|
||||
).toStrictEqual([
|
||||
'attribute.service.name',
|
||||
'log.body',
|
||||
'resource.service.name',
|
||||
'log.timestamp',
|
||||
'attribute:service.name',
|
||||
'log:body',
|
||||
'resource:service.name',
|
||||
'log:timestamp',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -329,11 +329,11 @@ describe('useOptionsMenu', () => {
|
||||
|
||||
result.current.config.addColumn?.onReorder([
|
||||
'state-indicator',
|
||||
'log.timestamp',
|
||||
'log:timestamp',
|
||||
'unknown.composite',
|
||||
'log.body',
|
||||
'resource.service.name',
|
||||
'attribute.service.name',
|
||||
'log:body',
|
||||
'resource:service.name',
|
||||
'attribute:service.name',
|
||||
]);
|
||||
|
||||
const reordered = mockUpdateColumns.mock.calls[0][0];
|
||||
@@ -341,13 +341,13 @@ describe('useOptionsMenu', () => {
|
||||
expect(
|
||||
reordered.map(
|
||||
(c: { name: string; fieldContext: string }) =>
|
||||
`${c.fieldContext}.${c.name}`,
|
||||
`${c.fieldContext}:${c.name}`,
|
||||
),
|
||||
).toStrictEqual([
|
||||
'log.timestamp',
|
||||
'log.body',
|
||||
'resource.service.name',
|
||||
'attribute.service.name',
|
||||
'log:timestamp',
|
||||
'log:body',
|
||||
'resource:service.name',
|
||||
'attribute:service.name',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -359,17 +359,17 @@ describe('useOptionsMenu', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// Removing 'resource.service.name' should drop ONLY the resource variant.
|
||||
result.current.config.addColumn?.onRemove('resource.service.name');
|
||||
// Removing 'resource:service.name' should drop ONLY the resource variant.
|
||||
result.current.config.addColumn?.onRemove('resource:service.name');
|
||||
|
||||
expect(mockUpdateColumns).toHaveBeenCalledTimes(1);
|
||||
const remaining = mockUpdateColumns.mock.calls[0][0];
|
||||
expect(
|
||||
remaining.map(
|
||||
(c: { name: string; fieldContext: string }) =>
|
||||
`${c.fieldContext}.${c.name}`,
|
||||
`${c.fieldContext}:${c.name}`,
|
||||
),
|
||||
).toStrictEqual(['log.body', 'attribute.service.name', 'log.timestamp']);
|
||||
).toStrictEqual(['log:body', 'attribute:service.name', 'log:timestamp']);
|
||||
});
|
||||
|
||||
it('removing by a non-matching composite ID is a no-op (filter returns the full list)', () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const getOptionsFromKeys = (
|
||||
};
|
||||
|
||||
// Composite identity for a column. Disambiguates same-name fields across
|
||||
// different fieldContexts (e.g. resource.service.name vs attribute.service.name).
|
||||
// different fieldContexts (e.g. resource:service.name vs attribute:service.name).
|
||||
// Falls back to bare name when context is missing.
|
||||
export const buildCompositeKey = (name: string, context?: string): string =>
|
||||
context ? `${context}.${name}` : name;
|
||||
context ? `${context}:${name}` : name;
|
||||
|
||||
@@ -2,17 +2,14 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Form, Select, Space } from 'antd';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { ModalFooterTitle } from 'container/PipelinePage/styles';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { ProcessorData } from 'types/api/pipeline/def';
|
||||
|
||||
import { formValidationRules } from '../config';
|
||||
import { ProcessorFormField } from './config';
|
||||
import { processorFields, ProcessorFormField } from './config';
|
||||
import CSVInput from './FormFields/CSVInput';
|
||||
import JsonFlattening from './FormFields/JsonFlattening';
|
||||
import { FormWrapper, PipelineIndexIcon, StyledSelect } from './styles';
|
||||
import { resolveProcessorFields } from './utils';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
@@ -136,23 +133,16 @@ function ProcessorForm({
|
||||
selectedProcessorData,
|
||||
isAdd,
|
||||
}: ProcessorFormProps): JSX.Element {
|
||||
const { featureFlags } = useAppContext();
|
||||
const isBodyJsonEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_JSON_BODY)
|
||||
?.active || false;
|
||||
|
||||
return (
|
||||
<div className="processor-form-container">
|
||||
{resolveProcessorFields(processorType, isBodyJsonEnabled).map(
|
||||
(fieldData: ProcessorFormField) => (
|
||||
<ProcessorFieldInput
|
||||
key={fieldData.name + String(fieldData.initialValue)}
|
||||
fieldData={fieldData}
|
||||
selectedProcessorData={selectedProcessorData}
|
||||
isAdd={isAdd}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{processorFields[processorType]?.map((fieldData: ProcessorFormField) => (
|
||||
<ProcessorFieldInput
|
||||
key={fieldData.name + String(fieldData.initialValue)}
|
||||
fieldData={fieldData}
|
||||
selectedProcessorData={selectedProcessorData}
|
||||
isAdd={isAdd}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { processorFields, ProcessorFormField } from './config';
|
||||
|
||||
const BODY_PARSE_FROM = 'body';
|
||||
const JSON_BODY_PARSE_FROM = 'body.message';
|
||||
|
||||
// With use_json_body the collector normalizes every body into a map before user
|
||||
// operators run, so a parser pointed at `body` gets a map it cannot read and
|
||||
// silently extracts nothing. The log text lives at body.message.
|
||||
export function resolveProcessorFields(
|
||||
processorType: string,
|
||||
isBodyJsonEnabled: boolean,
|
||||
): Array<ProcessorFormField> {
|
||||
const fields = processorFields[processorType] ?? [];
|
||||
|
||||
if (!isBodyJsonEnabled) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
return fields.map((field) =>
|
||||
field.name === 'parse_from' && field.initialValue === BODY_PARSE_FROM
|
||||
? { ...field, initialValue: JSON_BODY_PARSE_FROM }
|
||||
: field,
|
||||
);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { processorFields } from '../PipelineListsView/AddNewProcessor/config';
|
||||
import { resolveProcessorFields } from '../PipelineListsView/AddNewProcessor/utils';
|
||||
|
||||
const parseFromDefault = (
|
||||
fields: ReturnType<typeof resolveProcessorFields>,
|
||||
): unknown => fields.find((field) => field.name === 'parse_from')?.initialValue;
|
||||
|
||||
describe('resolveProcessorFields', () => {
|
||||
it.each(['grok_parser', 'regex_parser', 'json_parser'])(
|
||||
'defaults %s parse_from to body.message when use_json_body is on',
|
||||
(processorType) => {
|
||||
expect(parseFromDefault(resolveProcessorFields(processorType, true))).toBe(
|
||||
'body.message',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['grok_parser', 'regex_parser', 'json_parser'])(
|
||||
'keeps %s parse_from as body when use_json_body is off',
|
||||
(processorType) => {
|
||||
expect(parseFromDefault(resolveProcessorFields(processorType, false))).toBe(
|
||||
'body',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('leaves parse_from defaults that do not point at the body alone', () => {
|
||||
expect(parseFromDefault(resolveProcessorFields('time_parser', true))).toBe(
|
||||
'attributes.timestamp',
|
||||
);
|
||||
expect(
|
||||
parseFromDefault(resolveProcessorFields('severity_parser', true)),
|
||||
).toBe('attributes.logLevel');
|
||||
});
|
||||
|
||||
it('does not mutate the shared config', () => {
|
||||
resolveProcessorFields('grok_parser', true);
|
||||
|
||||
expect(parseFromDefault(processorFields.grok_parser)).toBe('body');
|
||||
});
|
||||
|
||||
it('returns an empty list for an unknown processor type', () => {
|
||||
expect(resolveProcessorFields('does_not_exist', true)).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,88 +0,0 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import useBaseAggregateOptions from '../useBaseAggregateOptions';
|
||||
|
||||
const mockGetUpdatedQuery = jest.fn();
|
||||
const mockNotificationsError = jest.fn();
|
||||
|
||||
jest.mock('container/GridCardLayout/useResolveQuery', () => ({
|
||||
__esModule: true,
|
||||
default: (): unknown => ({
|
||||
getUpdatedQuery: mockGetUpdatedQuery,
|
||||
isLoading: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
useNotifications: (): unknown => ({
|
||||
notifications: { error: mockNotificationsError },
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): unknown => ({ dashboardData: undefined }),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/dashboard/useContextVariables', () => ({
|
||||
__esModule: true,
|
||||
default: (): unknown => ({ processedVariables: {} }),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): unknown => ({ safeNavigate: jest.fn() }),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({ pathname: '/services/socky-api' }),
|
||||
}));
|
||||
|
||||
const QUERY = {
|
||||
builder: {
|
||||
queryData: [{ queryName: 'A', dataSource: 'traces', aggregations: [] }],
|
||||
},
|
||||
} as unknown as Query;
|
||||
|
||||
const AGGREGATE_DATA = { queryName: 'A', filters: [] };
|
||||
|
||||
const renderOptions = (): ReturnType<typeof renderHook> =>
|
||||
renderHook(() =>
|
||||
useBaseAggregateOptions({
|
||||
query: QUERY,
|
||||
onClose: jest.fn(),
|
||||
subMenu: '',
|
||||
setSubMenu: jest.fn(),
|
||||
aggregateData: AGGREGATE_DATA,
|
||||
fieldVariables: {},
|
||||
}),
|
||||
);
|
||||
|
||||
describe('useBaseAggregateOptions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('notifies and keeps the unresolved query when variable resolution fails', async () => {
|
||||
mockGetUpdatedQuery.mockRejectedValue(
|
||||
new Error('syntax errors in expression'),
|
||||
);
|
||||
|
||||
renderOptions();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockNotificationsError).toHaveBeenCalledWith({
|
||||
message: 'Unable to resolve variables',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not notify when variable resolution succeeds', async () => {
|
||||
mockGetUpdatedQuery.mockResolvedValue(QUERY);
|
||||
|
||||
renderOptions();
|
||||
|
||||
await waitFor(() => expect(mockGetUpdatedQuery).toHaveBeenCalled());
|
||||
expect(mockNotificationsError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,6 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import useUpdatedQuery from 'container/GridCardLayout/useResolveQuery';
|
||||
import { processContextLinks } from 'container/NewWidget/RightContainer/ContextLinks/utils';
|
||||
import useContextVariables from 'hooks/dashboard/useContextVariables';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import ContextMenu from 'periscope/components/ContextMenu';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { ContextLinksData } from 'types/api/dashboard/getAll';
|
||||
@@ -51,25 +50,23 @@ const useBaseAggregateOptions = ({
|
||||
const { getUpdatedQuery, isLoading: isResolveQueryLoading } =
|
||||
useUpdatedQuery();
|
||||
const { dashboardData } = useDashboardStore();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
useEffect(() => {
|
||||
if (!aggregateData) {
|
||||
return;
|
||||
}
|
||||
getUpdatedQuery({
|
||||
widgetConfig: {
|
||||
query,
|
||||
panelTypes: panelType || PANEL_TYPES.TIME_SERIES,
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
},
|
||||
dashboardData,
|
||||
})
|
||||
.then(setResolvedQuery)
|
||||
.catch(() => {
|
||||
setResolvedQuery(query);
|
||||
notifications.error({ message: 'Unable to resolve variables' });
|
||||
const resolveQuery = async (): Promise<void> => {
|
||||
const updatedQuery = await getUpdatedQuery({
|
||||
widgetConfig: {
|
||||
query,
|
||||
panelTypes: panelType || PANEL_TYPES.TIME_SERIES,
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
},
|
||||
dashboardData,
|
||||
});
|
||||
setResolvedQuery(updatedQuery);
|
||||
};
|
||||
resolveQuery();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [query, aggregateData, panelType]);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import ServiceAccountsSettings from '../ServiceAccountsSettings';
|
||||
const SA_LIST_ENDPOINT = '*/api/v1/service_accounts';
|
||||
const SA_ENDPOINT = '*/api/v1/service_accounts/:id';
|
||||
const SA_KEYS_ENDPOINT = '*/api/v1/service_accounts/:id/keys';
|
||||
const SA_ROLES_ENDPOINT = '*/api/v1/service_accounts/:id/roles';
|
||||
const ROLES_ENDPOINT = '*/api/v1/roles';
|
||||
|
||||
const mockServiceAccountsAPI = [
|
||||
@@ -17,7 +18,7 @@ const mockServiceAccountsAPI = [
|
||||
id: 'sa-1',
|
||||
name: 'CI Bot',
|
||||
email: 'ci-bot@signoz.io',
|
||||
serviceAccountRoles: [],
|
||||
roles: ['signoz-admin'],
|
||||
status: 'ACTIVE',
|
||||
createdAt: 1700000000,
|
||||
updatedAt: 1700000001,
|
||||
@@ -26,7 +27,7 @@ const mockServiceAccountsAPI = [
|
||||
id: 'sa-2',
|
||||
name: 'Monitoring Agent',
|
||||
email: 'monitor@signoz.io',
|
||||
serviceAccountRoles: [],
|
||||
roles: ['signoz-viewer'],
|
||||
status: 'ACTIVE',
|
||||
createdAt: 1700000002,
|
||||
updatedAt: 1700000003,
|
||||
@@ -35,7 +36,7 @@ const mockServiceAccountsAPI = [
|
||||
id: 'sa-3',
|
||||
name: 'Legacy Bot',
|
||||
email: 'legacy@signoz.io',
|
||||
serviceAccountRoles: [],
|
||||
roles: ['signoz-editor'],
|
||||
status: 'DISABLED',
|
||||
createdAt: 1700000004,
|
||||
updatedAt: 1700000005,
|
||||
@@ -60,6 +61,9 @@ describe('ServiceAccountsSettings (integration)', () => {
|
||||
rest.get(SA_KEYS_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ data: [] })),
|
||||
),
|
||||
rest.get(SA_ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ data: [] })),
|
||||
),
|
||||
rest.get(ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
|
||||
),
|
||||
|
||||
@@ -10,7 +10,7 @@ import DateTimeSelection from '../index';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from 'utils/getUnstableCurrentSearchParams';
|
||||
} from '../utils/getUnstableCurrentSearchParams';
|
||||
import { queryClient, TestWrapper } from './testUtils';
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
|
||||
@@ -10,7 +10,7 @@ import DateTimeSelection from '../index';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from 'utils/getUnstableCurrentSearchParams';
|
||||
} from '../utils/getUnstableCurrentSearchParams';
|
||||
import { queryClient, TestWrapper } from './testUtils';
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
|
||||
@@ -24,7 +24,7 @@ import DateTimeSelection from '../index';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from 'utils/getUnstableCurrentSearchParams';
|
||||
} from '../utils/getUnstableCurrentSearchParams';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
|
||||
@@ -5,7 +5,7 @@ import DateTimeSelection from '../index';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from 'utils/getUnstableCurrentSearchParams';
|
||||
} from '../utils/getUnstableCurrentSearchParams';
|
||||
import { queryClient, TestWrapper } from './testUtils';
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
|
||||
@@ -10,7 +10,7 @@ import DateTimeSelection from '../index';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from 'utils/getUnstableCurrentSearchParams';
|
||||
} from '../utils/getUnstableCurrentSearchParams';
|
||||
import { queryClient, TestWrapper, createMockMoment } from './testUtils';
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
Time,
|
||||
TimeRange,
|
||||
} from './types';
|
||||
import { getUnstableCurrentSearchParams } from 'utils/getUnstableCurrentSearchParams';
|
||||
import { getUnstableCurrentSearchParams } from './utils/getUnstableCurrentSearchParams';
|
||||
|
||||
import './DateTimeSelectionV2.styles.scss';
|
||||
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import {
|
||||
useCreateServiceAccountRole,
|
||||
useDeleteServiceAccountRole,
|
||||
useGetServiceAccount,
|
||||
getGetServiceAccountRolesQueryKey,
|
||||
useCreateServiceAccountRoleDeprecated,
|
||||
useDeleteServiceAccountRoleDeprecated,
|
||||
useGetServiceAccountRoles,
|
||||
} from 'api/generated/services/serviceaccount';
|
||||
import type {
|
||||
AuthtypesGettableRoleDTO,
|
||||
ServiceaccounttypesServiceAccountRoleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { AuthtypesGettableRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { retryOn429 } from 'utils/errorUtils';
|
||||
|
||||
const enum PromiseStatus {
|
||||
Fulfilled = 'fulfilled',
|
||||
Rejected = 'rejected',
|
||||
}
|
||||
|
||||
// Stable identity so the memos below do not recompute on every render.
|
||||
const EMPTY_SERVICE_ACCOUNT_ROLES: ServiceaccounttypesServiceAccountRoleDTO[] =
|
||||
[];
|
||||
|
||||
export interface RoleUpdateFailure {
|
||||
roleName: string;
|
||||
error: unknown;
|
||||
@@ -37,40 +33,34 @@ export function useServiceAccountRoleManager(
|
||||
accountId: string,
|
||||
options?: { enabled?: boolean },
|
||||
): UseServiceAccountRoleManagerResult {
|
||||
const { data, isLoading } = useGetServiceAccount(
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading } = useGetServiceAccountRoles(
|
||||
{ id: accountId },
|
||||
{ query: { enabled: options?.enabled ?? true } },
|
||||
);
|
||||
|
||||
const serviceAccountRoles =
|
||||
data?.data?.serviceAccountRoles ?? EMPTY_SERVICE_ACCOUNT_ROLES;
|
||||
|
||||
const currentRoles = useMemo<AuthtypesGettableRoleDTO[]>(
|
||||
() =>
|
||||
serviceAccountRoles.map((serviceAccountRole) => serviceAccountRole.role),
|
||||
[serviceAccountRoles],
|
||||
);
|
||||
|
||||
// DELETE /api/v1/service_account_roles/{id} is keyed by the join row, not the role.
|
||||
const assignmentIdByRoleId = useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
serviceAccountRoles.map((serviceAccountRole) => [
|
||||
serviceAccountRole.roleId,
|
||||
serviceAccountRole.id,
|
||||
]),
|
||||
),
|
||||
[serviceAccountRoles],
|
||||
() => data?.data ?? [],
|
||||
[data?.data],
|
||||
);
|
||||
|
||||
// the retry for these mutations is safe due to being idempotent on backend
|
||||
const { mutateAsync: createRole } = useCreateServiceAccountRole({
|
||||
const { mutateAsync: createRole } = useCreateServiceAccountRoleDeprecated({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
const { mutateAsync: deleteRole } = useDeleteServiceAccountRole({
|
||||
const { mutateAsync: deleteRole } = useDeleteServiceAccountRoleDeprecated({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
|
||||
const invalidateRoles = useCallback(
|
||||
() =>
|
||||
queryClient.invalidateQueries(
|
||||
getGetServiceAccountRolesQueryKey({ id: accountId }),
|
||||
),
|
||||
[accountId, queryClient],
|
||||
);
|
||||
|
||||
const applyDiff = useCallback(
|
||||
async (
|
||||
localRoleIds: string[],
|
||||
@@ -94,34 +84,26 @@ export function useServiceAccountRoleManager(
|
||||
...addedRoles.map((role) => ({
|
||||
role,
|
||||
run: (): ReturnType<typeof createRole> =>
|
||||
createRole({
|
||||
data: { serviceAccountId: accountId, roleId: role.id ?? '' },
|
||||
}),
|
||||
createRole({ pathParams: { id: accountId }, data: { id: role.id } }),
|
||||
})),
|
||||
...removedRoles.map((role) => ({
|
||||
role,
|
||||
run: (): ReturnType<typeof deleteRole> =>
|
||||
deleteRole({ pathParams: { id: accountId, rid: role.id ?? '' } }),
|
||||
})),
|
||||
...removedRoles
|
||||
.map((role) => ({
|
||||
role,
|
||||
assignmentId: assignmentIdByRoleId.get(role.id ?? ''),
|
||||
}))
|
||||
.filter(
|
||||
(
|
||||
entry,
|
||||
): entry is {
|
||||
role: AuthtypesGettableRoleDTO;
|
||||
assignmentId: string;
|
||||
} => !!entry.assignmentId,
|
||||
)
|
||||
.map(({ role, assignmentId }) => ({
|
||||
role,
|
||||
run: (): ReturnType<typeof deleteRole> =>
|
||||
deleteRole({ pathParams: { id: assignmentId } }),
|
||||
})),
|
||||
];
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
allOperations.map((op) => op.run()),
|
||||
);
|
||||
|
||||
const successCount = results.filter(
|
||||
(r) => r.status === PromiseStatus.Fulfilled,
|
||||
).length;
|
||||
if (successCount > 0) {
|
||||
await invalidateRoles();
|
||||
}
|
||||
|
||||
const failures: RoleUpdateFailure[] = [];
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === PromiseStatus.Rejected) {
|
||||
@@ -131,6 +113,7 @@ export function useServiceAccountRoleManager(
|
||||
error: result.reason,
|
||||
onRetry: async (): Promise<void> => {
|
||||
await run();
|
||||
await invalidateRoles();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -138,7 +121,7 @@ export function useServiceAccountRoleManager(
|
||||
|
||||
return failures;
|
||||
},
|
||||
[accountId, currentRoles, assignmentIdByRoleId, createRole, deleteRole],
|
||||
[accountId, currentRoles, createRole, deleteRole, invalidateRoles],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -189,8 +189,7 @@ function DashboardActions({
|
||||
onClick: (): void => void handleClone(),
|
||||
});
|
||||
}
|
||||
|
||||
if (canEditDashboard && (isAuthor || user.role === USER_ROLES.ADMIN)) {
|
||||
if (isAuthor || user.role === USER_ROLES.ADMIN) {
|
||||
dashboardGroup.push({
|
||||
key: 'lock',
|
||||
label: isDashboardLocked ? 'Unlock dashboard' : 'Lock dashboard',
|
||||
|
||||
@@ -46,11 +46,23 @@ beforeAll(() => {
|
||||
});
|
||||
|
||||
// jest.config maps the real hook to a no-op mock; this suite needs real navigation.
|
||||
jest.mock('hooks/useSafeNavigate', () =>
|
||||
jest
|
||||
.requireActual('tests/browser-history-safe-navigate')
|
||||
.createBrowserHistorySafeNavigateMock(),
|
||||
);
|
||||
jest.mock('hooks/useSafeNavigate', () => {
|
||||
const { useHistory } = jest.requireActual('react-router-dom');
|
||||
return {
|
||||
useSafeNavigate: (): unknown => {
|
||||
const history = useHistory();
|
||||
return {
|
||||
safeNavigate: (to: string, opts?: { replace?: boolean }): void => {
|
||||
if (opts?.replace) {
|
||||
history.replace(to);
|
||||
} else {
|
||||
history.push(to);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
|
||||
getKeySuggestions: jest
|
||||
@@ -192,12 +204,9 @@ function Harness(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
const INITIAL_ROUTE = '/dashboard/dash-1';
|
||||
|
||||
const renderHarness = (): void => {
|
||||
window.history.replaceState(null, '', INITIAL_ROUTE);
|
||||
render(
|
||||
<MemoryRouter initialEntries={[INITIAL_ROUTE]}>
|
||||
<MemoryRouter initialEntries={['/dashboard/dash-1']}>
|
||||
<CompatRouter>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<ReduxProvider store={configureStore([])(appStore.getState())}>
|
||||
|
||||
@@ -16,11 +16,23 @@ import ViewPanelModal from '../ViewPanelModal/ViewPanelModal';
|
||||
import { useViewPanel } from '../hooks/useViewPanel';
|
||||
|
||||
// jest.config maps the real hook to a no-op mock; this suite needs real navigation.
|
||||
jest.mock('hooks/useSafeNavigate', () =>
|
||||
jest
|
||||
.requireActual('tests/browser-history-safe-navigate')
|
||||
.createBrowserHistorySafeNavigateMock(),
|
||||
);
|
||||
jest.mock('hooks/useSafeNavigate', () => {
|
||||
const { useHistory } = jest.requireActual('react-router-dom');
|
||||
return {
|
||||
useSafeNavigate: (): unknown => {
|
||||
const history = useHistory();
|
||||
return {
|
||||
safeNavigate: (to: string, opts?: { replace?: boolean }): void => {
|
||||
if (opts?.replace) {
|
||||
history.replace(to);
|
||||
} else {
|
||||
history.push(to);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock(
|
||||
'pages/DashboardPageV2/DashboardContainer/hooks/usePanelQuery',
|
||||
@@ -138,12 +150,9 @@ function Harness(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
const INITIAL_ROUTE = '/dashboard/dash-1';
|
||||
|
||||
const renderHarness = (): void => {
|
||||
window.history.replaceState(null, '', INITIAL_ROUTE);
|
||||
render(
|
||||
<MemoryRouter initialEntries={[INITIAL_ROUTE]}>
|
||||
<MemoryRouter initialEntries={['/dashboard/dash-1']}>
|
||||
<CompatRouter>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<ReduxProvider store={configureStore([])(appStore.getState())}>
|
||||
|
||||
@@ -14,11 +14,23 @@ import { useViewPanelMode } from '../ViewPanelModal/useViewPanelMode';
|
||||
import { useViewPanel } from '../hooks/useViewPanel';
|
||||
|
||||
// jest.config maps the real hook to a no-op mock; this suite needs real navigation.
|
||||
jest.mock('hooks/useSafeNavigate', () =>
|
||||
jest
|
||||
.requireActual('tests/browser-history-safe-navigate')
|
||||
.createBrowserHistorySafeNavigateMock(),
|
||||
);
|
||||
jest.mock('hooks/useSafeNavigate', () => {
|
||||
const { useHistory } = jest.requireActual('react-router-dom');
|
||||
return {
|
||||
useSafeNavigate: (): unknown => {
|
||||
const history = useHistory();
|
||||
return {
|
||||
safeNavigate: (to: string, opts?: { replace?: boolean }): void => {
|
||||
if (opts?.replace) {
|
||||
history.replace(to);
|
||||
} else {
|
||||
history.push(to);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock(
|
||||
'pages/DashboardPageV2/DashboardContainer/hooks/usePanelQuery',
|
||||
@@ -172,12 +184,9 @@ function Harness(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
const INITIAL_ROUTE = '/dashboard/dash-1';
|
||||
|
||||
const renderHarness = (): void => {
|
||||
window.history.replaceState(null, '', INITIAL_ROUTE);
|
||||
render(
|
||||
<MemoryRouter initialEntries={[INITIAL_ROUTE]}>
|
||||
<MemoryRouter initialEntries={['/dashboard/dash-1']}>
|
||||
<CompatRouter>
|
||||
<QueryBuilderProvider>
|
||||
<Harness />
|
||||
|
||||
@@ -19,20 +19,11 @@ import { resolveDashboardImage } from 'pages/DashboardPageV2/DashboardContainer/
|
||||
interface DashboardContainerProps {
|
||||
dashboard: DashboardtypesGettableDashboardV2DTO;
|
||||
refetch: () => void;
|
||||
/**
|
||||
* @deprecated
|
||||
* `canEditDashboardOverride` is a temporary solution to allow the dashboard to be view only.
|
||||
* This is only used for LLM Observability.
|
||||
* It will be removed in the future.
|
||||
* TODO: @Ashwin / @Abhi — remove when the final solution is implemented.
|
||||
*/
|
||||
canEditDashboardOverride?: boolean;
|
||||
}
|
||||
|
||||
function DashboardContainer({
|
||||
dashboard,
|
||||
refetch,
|
||||
canEditDashboardOverride,
|
||||
}: DashboardContainerProps): JSX.Element {
|
||||
const spec = dashboard.spec;
|
||||
const image = resolveDashboardImage(dashboard.image);
|
||||
@@ -54,11 +45,10 @@ function DashboardContainer({
|
||||
// Seed during render (not an effect) so the first Panel render already sees the id —
|
||||
// useDashboardFetchRequired throws on a missing id. setEditContext self-guards.
|
||||
const setEditContext = useDashboardStore((s) => s.setEditContext);
|
||||
|
||||
setEditContext({
|
||||
dashboardId: dashboard.id,
|
||||
isLocked,
|
||||
canEditDashboard: canEditDashboardOverride ?? canEditDashboard,
|
||||
canEditDashboard,
|
||||
refetch,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CSSProperties, useMemo, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import CopyButton from 'periscope/components/CopyButton/CopyButton';
|
||||
@@ -24,23 +24,16 @@ export interface DataViewerProps {
|
||||
data: Record<string, any>;
|
||||
drawerKey?: string;
|
||||
prettyViewProps?: Omit<PrettyViewProps, 'data' | 'drawerKey'>;
|
||||
// Optional override for the JSON view otherwise `data` is
|
||||
// stringified and used.
|
||||
jsonString?: string;
|
||||
fontSize?: number;
|
||||
}
|
||||
|
||||
function DataViewer({
|
||||
data,
|
||||
drawerKey = 'default',
|
||||
prettyViewProps,
|
||||
jsonString,
|
||||
fontSize,
|
||||
}: DataViewerProps): JSX.Element {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>(ViewMode.Pretty);
|
||||
|
||||
const derivedJson = useMemo(() => JSON.stringify(data, null, 2), [data]);
|
||||
const json = jsonString ?? derivedJson;
|
||||
const jsonString = useMemo(() => JSON.stringify(data, null, 2), [data]);
|
||||
|
||||
const handleViewModeChange = (value: string): void => {
|
||||
const next = value as ViewMode;
|
||||
@@ -62,14 +55,7 @@ function DataViewer({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="data-viewer"
|
||||
style={
|
||||
fontSize
|
||||
? ({ '--data-viewer-font-size': `${fontSize}px` } as CSSProperties)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="data-viewer">
|
||||
<div className="data-viewer__toolbar">
|
||||
<ToggleGroupSimple
|
||||
type="single"
|
||||
@@ -79,14 +65,14 @@ function DataViewer({
|
||||
items={VIEW_MODE_OPTIONS}
|
||||
testId="data-viewer-view-mode"
|
||||
/>
|
||||
<CopyButton value={json} ariaLabel="Copy JSON" />
|
||||
<CopyButton value={jsonString} ariaLabel="Copy JSON" />
|
||||
</div>
|
||||
|
||||
<div className="data-viewer__content">
|
||||
{viewMode === ViewMode.Pretty && (
|
||||
<PrettyView data={data} drawerKey={drawerKey} {...prettyViewProps} />
|
||||
)}
|
||||
{viewMode === ViewMode.Json && <JsonView data={json} fontSize={fontSize} />}
|
||||
{viewMode === ViewMode.Json && <JsonView data={jsonString} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,6 @@ import './JsonView.styles.scss';
|
||||
export interface JsonViewProps {
|
||||
data: string;
|
||||
height?: string;
|
||||
fontSize?: number;
|
||||
}
|
||||
|
||||
const editorOptions: EditorProps['options'] = {
|
||||
@@ -57,11 +56,7 @@ function setEditorTheme(monaco: Monaco): void {
|
||||
});
|
||||
}
|
||||
|
||||
function JsonView({
|
||||
data,
|
||||
height = '575px',
|
||||
fontSize = 12,
|
||||
}: JsonViewProps): JSX.Element {
|
||||
function JsonView({ data, height = '575px' }: JsonViewProps): JSX.Element {
|
||||
const [isWrapWord, setIsWrapWord] = useState(true);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
@@ -70,11 +65,7 @@ function JsonView({
|
||||
<MEditor
|
||||
value={data}
|
||||
language="json"
|
||||
options={{
|
||||
...editorOptions,
|
||||
fontSize,
|
||||
wordWrap: isWrapWord ? 'on' : 'off',
|
||||
}}
|
||||
options={{ ...editorOptions, wordWrap: isWrapWord ? 'on' : 'off' }}
|
||||
onChange={(): void => {}}
|
||||
height={height}
|
||||
theme={isDarkMode ? 'signoz-dark' : 'light'}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
border-bottom: 1px solid var(--l2-border) !important;
|
||||
border-radius: 0 !important;
|
||||
font-family: 'SF Mono', 'Geist Mono', 'Fira Code', monospace !important;
|
||||
font-size: var(--data-viewer-font-size, 12px) !important;
|
||||
font-size: 12px !important;
|
||||
line-height: 18px !important;
|
||||
background: var(--l1-background) !important;
|
||||
outline: none !important;
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
// Font spec: SF Mono, 12px, 400, 18px line-height, -0.5% letter-spacing
|
||||
font-family: 'SF Mono', 'Geist Mono', 'Fira Code', monospace;
|
||||
font-size: var(--data-viewer-font-size, 12px);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
@@ -77,17 +77,6 @@
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
// Pin the caret to the leaf padding-left (1.25em) so nested keys align with leaf
|
||||
// keys at every depth; inline-flex centers the glyph on the text line.
|
||||
&__arrow {
|
||||
box-sizing: border-box !important;
|
||||
width: 1.25em !important;
|
||||
padding-right: 0 !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
height: 18px; // match the row line-height so the caret centers on the text
|
||||
}
|
||||
|
||||
// Leaf node row — full-width hover highlight
|
||||
&__row {
|
||||
display: flex !important;
|
||||
@@ -95,11 +84,6 @@
|
||||
position: relative;
|
||||
isolation: isolate; // own stacking context so ::before sits behind content, not the panel bg
|
||||
|
||||
// react-json-tree indents leaf values with a depth-dependent padding-left
|
||||
// (2.125em when nested vs 1.25em at top); we indent via `ul`, so pin it flat
|
||||
// to keep leaf and nested keys aligned at every depth.
|
||||
padding-left: 1.25em !important;
|
||||
|
||||
// Keep actions visible on hover, or while this row's menu is open
|
||||
&:hover .pretty-view__actions,
|
||||
&:has(> span [data-state='open']) .pretty-view__actions {
|
||||
@@ -191,13 +175,6 @@
|
||||
&__row &__value-row {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
// Anchor the ... to the top of the value (not centered in a tall value)
|
||||
align-items: flex-start;
|
||||
|
||||
.pretty-view__actions {
|
||||
position: sticky;
|
||||
top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// ... actions button — hidden by default, shown on row hover
|
||||
|
||||
@@ -67,15 +67,6 @@ export interface PrettyViewProps {
|
||||
*/
|
||||
pinnedFieldsValue?: string[];
|
||||
onPinnedFieldsChange?: (next: string[]) => void;
|
||||
/**
|
||||
* Optional per-leaf value renderer. Return a node to override the default
|
||||
* `String(value)` rendering for that leaf, or `undefined` to fall back. Used
|
||||
* e.g. to sanitize/format log body values (ANSI → color, unescape).
|
||||
*/
|
||||
renderLeafValue?: (
|
||||
value: unknown,
|
||||
keyPath: readonly (string | number)[],
|
||||
) => React.ReactNode | undefined;
|
||||
}
|
||||
|
||||
function PrettyView({
|
||||
@@ -87,7 +78,6 @@ function PrettyView({
|
||||
drawerKey = 'default',
|
||||
pinnedFieldsValue,
|
||||
onPinnedFieldsChange,
|
||||
renderLeafValue,
|
||||
}: PrettyViewProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
@@ -243,9 +233,7 @@ function PrettyView({
|
||||
>
|
||||
<span
|
||||
className="pretty-view__actions"
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
@@ -290,16 +278,15 @@ function PrettyView({
|
||||
...keyPath: KeyPath
|
||||
): React.ReactNode => {
|
||||
const forwardPath = keyPathToForward(keyPath);
|
||||
const custom = renderLeafValue?.(value, keyPath);
|
||||
return renderWithActions({
|
||||
content: custom ?? String(valueAsString),
|
||||
content: String(valueAsString),
|
||||
fieldKey: keyPathToDisplayString(keyPath),
|
||||
fieldKeyPath: forwardPath,
|
||||
value,
|
||||
isNested: typeof value === 'object' && value !== null,
|
||||
});
|
||||
},
|
||||
[renderWithActions, renderLeafValue],
|
||||
[renderWithActions],
|
||||
);
|
||||
|
||||
const pinnedLabelRenderer = useCallback(
|
||||
|
||||
@@ -59,12 +59,4 @@ export const themeExtension = {
|
||||
style: { ...style },
|
||||
className: 'pretty-view__nested-row',
|
||||
}),
|
||||
arrowContainer: (
|
||||
{ style }: { style: Record<string, unknown> },
|
||||
arrowStyle: string,
|
||||
): { style: Record<string, unknown>; className?: string } => ({
|
||||
style: { ...style },
|
||||
// Only pin the single caret's width (for key alignment).
|
||||
className: arrowStyle === 'double' ? undefined : 'pretty-view__arrow',
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -37,6 +37,7 @@ import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
||||
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
||||
import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType';
|
||||
@@ -65,7 +66,6 @@ import {
|
||||
} from 'types/common/queryBuilder';
|
||||
import { sanitizeOrderByForExplorer } from 'utils/sanitizeOrderBy';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { getUnstableCurrentSearchParams } from 'utils/getUnstableCurrentSearchParams';
|
||||
|
||||
export const QueryBuilderContext = createContext<QueryBuilderContextType>({
|
||||
currentQuery: initialQueriesMap.metrics,
|
||||
@@ -105,6 +105,7 @@ export const QueryBuilderContext = createContext<QueryBuilderContextType>({
|
||||
export function QueryBuilderProvider({
|
||||
children,
|
||||
}: PropsWithChildren): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const location = useLocation();
|
||||
|
||||
const currentPathnameRef = useRef<string | null>(location.pathname);
|
||||
@@ -121,7 +122,7 @@ export function QueryBuilderProvider({
|
||||
null,
|
||||
);
|
||||
|
||||
const panelTypeQueryParams = getUnstableCurrentSearchParams().get(
|
||||
const panelTypeQueryParams = urlQuery.get(
|
||||
QueryParams.panelTypes,
|
||||
) as PANEL_TYPES | null;
|
||||
|
||||
@@ -975,7 +976,6 @@ export function QueryBuilderProvider({
|
||||
unit: query.unit || initialQueryState.unit,
|
||||
};
|
||||
|
||||
const urlQuery = getUnstableCurrentSearchParams();
|
||||
const pagination = urlQuery.get(QueryParams.pagination);
|
||||
|
||||
if (pagination) {
|
||||
@@ -1014,7 +1014,7 @@ export function QueryBuilderProvider({
|
||||
|
||||
safeNavigate(generatedUrl, { newTab });
|
||||
},
|
||||
[location.pathname, safeNavigate],
|
||||
[location.pathname, safeNavigate, urlQuery],
|
||||
);
|
||||
|
||||
const handleSetConfig = useCallback(
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
// Mock factory for suites that need `useSafeNavigate` to navigate for real.
|
||||
//
|
||||
// `jest.config.ts` maps every `hooks/useSafeNavigate` import to the no-op
|
||||
// `__tests__/safeNavigateMock.ts`, so a suite that drives navigation has to opt
|
||||
// out with its own `jest.mock`.
|
||||
//
|
||||
// In production `safeNavigate` goes through `createBrowserHistory`, which writes
|
||||
// `window.location` as well as notifying the router. `MemoryRouter` never touches
|
||||
// `window`, so anything reading `getUnstableCurrentSearchParams()` sees an empty
|
||||
// search and drops the params the test just navigated with. This mock writes both.
|
||||
//
|
||||
// The `jest.mock` factory is hoisted above imports, so require it inside:
|
||||
//
|
||||
// jest.mock('hooks/useSafeNavigate', () =>
|
||||
// jest
|
||||
// .requireActual('tests/browser-history-safe-navigate')
|
||||
// .createBrowserHistorySafeNavigateMock(),
|
||||
// );
|
||||
|
||||
import type { History } from 'history';
|
||||
|
||||
interface SafeNavigateOptions {
|
||||
replace?: boolean;
|
||||
}
|
||||
|
||||
interface UseSafeNavigateModule {
|
||||
useSafeNavigate: () => {
|
||||
safeNavigate: (to: string, options?: SafeNavigateOptions) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function createBrowserHistorySafeNavigateMock(): UseSafeNavigateModule {
|
||||
const { useHistory } = jest.requireActual<{ useHistory: () => History }>(
|
||||
'react-router-dom',
|
||||
);
|
||||
|
||||
return {
|
||||
useSafeNavigate: () => {
|
||||
const history = useHistory();
|
||||
|
||||
return {
|
||||
safeNavigate: (to: string, options?: SafeNavigateOptions): void => {
|
||||
if (options?.replace) {
|
||||
window.history.replaceState(null, '', to);
|
||||
history.replace(to);
|
||||
} else {
|
||||
window.history.pushState(null, '', to);
|
||||
history.push(to);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -29,7 +29,6 @@ type OmitAttributesResources = Pick<
|
||||
ILog,
|
||||
Exclude<
|
||||
keyof ILog,
|
||||
| 'date'
|
||||
| 'resources_string'
|
||||
| 'scope_string'
|
||||
| 'attributesString'
|
||||
@@ -41,6 +40,6 @@ type OmitAttributesResources = Pick<
|
||||
|
||||
export type ILogAggregateAttributesResources = OmitAttributesResources & {
|
||||
attributes: Record<string, never>;
|
||||
resource: Record<string, never>;
|
||||
resources: Record<string, never>;
|
||||
scope: Record<string, never>;
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
|
||||
@@ -185,7 +186,18 @@ func (n *Notifier) Notify(ctx context.Context, alerts ...*types.Alert) (bool, er
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := notify.PostJSON(ctx, n.client, n.conf.WebhookURL.String(), buf) //nolint:bodyclose
|
||||
// Thread same-rule alerts together: threadKey is a stable hash of the
|
||||
// alert group key. Changing a rule's grouping starts a new thread.
|
||||
u, err := url.Parse(n.conf.WebhookURL.String())
|
||||
if err != nil {
|
||||
return false, errors.WrapInternalf(err, errors.CodeInternal, "parse google chat webhook url")
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("threadKey", key.Hash())
|
||||
q.Set("messageReplyOption", "REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD")
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
resp, err := notify.PostJSON(ctx, n.client, u.String(), buf) //nolint:bodyclose
|
||||
if err != nil {
|
||||
return true, notify.RedactURL(err)
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ func TestGoogleChatMessageSizeLimit(t *testing.T) {
|
||||
assert.LessOrEqual(t, bodyLen, maxMessageBytes, "posted body must be within the size limit")
|
||||
}
|
||||
|
||||
func TestGoogleChatWebhookURLVerbatim(t *testing.T) {
|
||||
func TestGoogleChatThreading(t *testing.T) {
|
||||
var query url.Values
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
query = r.URL.Query()
|
||||
@@ -253,11 +253,25 @@ func TestGoogleChatWebhookURLVerbatim(t *testing.T) {
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
n := newTestNotifier(t, server.URL+"?key=abc&token=xyz", "T", "")
|
||||
_, err := n.Notify(newTestContext(), newTestAlerts("X")...)
|
||||
require.NoError(t, err)
|
||||
cases := []struct{ name, groupKey string }{
|
||||
{"rule a", "{ruleId=\"aaa\"}"},
|
||||
{"rule b", "{ruleId=\"bbb\"}"},
|
||||
}
|
||||
seen := map[string]string{}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
n := newTestNotifier(t, server.URL, "T", "")
|
||||
ctx := notify.WithGroupKey(context.Background(), c.groupKey)
|
||||
_, err := n.Notify(ctx, newTestAlerts("X")...)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, url.Values{"key": {"abc"}, "token": {"xyz"}}, query, "configured webhook URL must be posted verbatim, with no params added")
|
||||
assert.Equal(t, "REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD", query.Get("messageReplyOption"))
|
||||
threadKey := query.Get("threadKey")
|
||||
assert.Equal(t, notify.Key(c.groupKey).Hash(), threadKey, "threadKey must be the group key hash")
|
||||
seen[c.name] = threadKey
|
||||
})
|
||||
}
|
||||
assert.NotEqual(t, seen["rule a"], seen["rule b"], "distinct group keys must yield distinct threadKeys")
|
||||
}
|
||||
|
||||
func TestGoogleChatCustomTemplateMarkdown(t *testing.T) {
|
||||
|
||||
@@ -3,17 +3,16 @@ package flagger
|
||||
import "github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
|
||||
var (
|
||||
FeatureUseSpanMetrics = featuretypes.MustNewName("use_span_metrics")
|
||||
FeatureKafkaSpanEval = featuretypes.MustNewName("kafka_span_eval")
|
||||
FeatureHideRootUser = featuretypes.MustNewName("hide_root_user")
|
||||
FeatureGetMetersFromZeus = featuretypes.MustNewName("get_meters_from_zeus")
|
||||
FeaturePutMetersInZeus = featuretypes.MustNewName("put_meters_in_zeus")
|
||||
FeatureUseMeterReporter = featuretypes.MustNewName("use_meter_reporter")
|
||||
FeatureUseJSONBody = featuretypes.MustNewName("use_json_body")
|
||||
FeatureEnableAIObservability = featuretypes.MustNewName("enable_ai_observability")
|
||||
FeatureEnableMetricsReduction = featuretypes.MustNewName("enable_metrics_reduction")
|
||||
FeatureUseSpanMetrics = featuretypes.MustNewName("use_span_metrics")
|
||||
FeatureKafkaSpanEval = featuretypes.MustNewName("kafka_span_eval")
|
||||
FeatureHideRootUser = featuretypes.MustNewName("hide_root_user")
|
||||
FeatureGetMetersFromZeus = featuretypes.MustNewName("get_meters_from_zeus")
|
||||
FeaturePutMetersInZeus = featuretypes.MustNewName("put_meters_in_zeus")
|
||||
FeatureUseMeterReporter = featuretypes.MustNewName("use_meter_reporter")
|
||||
FeatureUseJSONBody = featuretypes.MustNewName("use_json_body")
|
||||
FeatureEnableAIObservability = featuretypes.MustNewName("enable_ai_observability")
|
||||
FeatureEnableMetricsReduction = featuretypes.MustNewName("enable_metrics_reduction")
|
||||
FeatureUsePrometheusClickhouseV2 = featuretypes.MustNewName("use_prometheus_clickhouse_v2")
|
||||
FeatureResolveSemconvFamilies = featuretypes.MustNewName("resolve_semconv_families")
|
||||
)
|
||||
|
||||
func MustNewRegistry() featuretypes.Registry {
|
||||
@@ -98,14 +97,6 @@ func MustNewRegistry() featuretypes.Registry {
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureResolveSemconvFamilies,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
Stage: featuretypes.StageExperimental,
|
||||
Description: "Controls whether trace queries resolve a semantic-convention name to all the spellings of its family",
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -51,28 +51,6 @@
|
||||
},
|
||||
"name": "Region"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "ListVariable",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "FunctionName",
|
||||
"description": "Name of the Lambda function"
|
||||
},
|
||||
"allowAllValue": true,
|
||||
"allowMultiple": true,
|
||||
"customAllValue": "",
|
||||
"capturingRegexp": "",
|
||||
"sort": "none",
|
||||
"plugin": {
|
||||
"kind": "signoz/DynamicVariable",
|
||||
"spec": {
|
||||
"name": "FunctionName",
|
||||
"signal": "metrics"
|
||||
}
|
||||
},
|
||||
"name": "FunctionName"
|
||||
}
|
||||
}
|
||||
],
|
||||
"panels": {
|
||||
@@ -140,7 +118,7 @@
|
||||
],
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS) AND FunctionName IN $FunctionName"
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS)"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
@@ -240,7 +218,7 @@
|
||||
],
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS) AND FunctionName IN $FunctionName"
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS)"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
@@ -340,7 +318,7 @@
|
||||
],
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS) AND FunctionName IN $FunctionName"
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS)"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
@@ -440,7 +418,7 @@
|
||||
],
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS) AND FunctionName IN $FunctionName"
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS)"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
@@ -540,7 +518,7 @@
|
||||
],
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS) AND FunctionName IN $FunctionName"
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS)"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
@@ -640,7 +618,7 @@
|
||||
],
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS) AND FunctionName IN $FunctionName"
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS)"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
@@ -740,7 +718,7 @@
|
||||
],
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS) AND FunctionName IN $FunctionName"
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS)"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
@@ -853,4 +831,4 @@
|
||||
"refreshInterval": "",
|
||||
"links": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,10 +40,7 @@ func (c *conditionBuilder) ConditionFor(
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Rule state history fields have no family support, so every logical field
|
||||
// is single-member and flattens losslessly to its physical key.
|
||||
resolved, warning := querybuilder.ResolveLogicalFields(key, querybuilder.MatchingLogicalFields(ctx, orgID, nil, key, fieldKeys))
|
||||
keys := querybuilder.SingleKeys(resolved)
|
||||
keys, warning := querybuilder.ResolveKeys(key, querybuilder.MatchingFieldKeys(key, fieldKeys))
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
@@ -141,9 +138,9 @@ func (c *conditionBuilder) conditionForKey(
|
||||
return "true", nil
|
||||
}
|
||||
if operator == qbtypes.FilterOperatorExists {
|
||||
return fmt.Sprintf("JSONHas(labels, %s)", sb.Var(key.Name)), nil
|
||||
return fmt.Sprintf("has(JSONExtractKeys(labels), %s)", sb.Var(key.Name)), nil
|
||||
}
|
||||
return fmt.Sprintf("not JSONHas(labels, %s)", sb.Var(key.Name)), nil
|
||||
return fmt.Sprintf("not has(JSONExtractKeys(labels), %s)", sb.Var(key.Name)), nil
|
||||
}
|
||||
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported operator: %v", operator)
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"strings"
|
||||
|
||||
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -52,7 +51,7 @@ func (m *fieldMapper) FieldFor(ctx context.Context, _ valuer.UUID, _, _ uint64,
|
||||
return "", err
|
||||
}
|
||||
if col.Name == "labels" && key.Name != "labels" {
|
||||
return fmt.Sprintf("JSONExtractString(labels, %s)", querybuilder.ClickHouseStringLiteral(key.Name)), nil
|
||||
return fmt.Sprintf("JSONExtractString(labels, '%s')", strings.ReplaceAll(key.Name, "'", "\\'")), nil
|
||||
}
|
||||
return col.Name, nil
|
||||
}
|
||||
@@ -65,24 +64,6 @@ func (m *fieldMapper) ColumnFor(ctx context.Context, _ valuer.UUID, _, _ uint64,
|
||||
return []*schema.Column{col}, nil
|
||||
}
|
||||
|
||||
// ExistsFor implements the per-key existence primitive of qbtypes.FieldMapper.
|
||||
// A label inside the JSON gets a membership check, with the same condition
|
||||
// that FieldFor uses for extraction; every real column always exists.
|
||||
func (m *fieldMapper) ExistsFor(ctx context.Context, _ valuer.UUID, _, _ uint64, key *telemetrytypes.TelemetryFieldKey, exists bool) (string, error) {
|
||||
col, err := m.getColumn(ctx, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if col.Name == "labels" && key.Name != "labels" {
|
||||
pred := fmt.Sprintf("JSONHas(labels, %s)", querybuilder.ClickHouseStringLiteral(key.Name))
|
||||
if exists {
|
||||
return pred, nil
|
||||
}
|
||||
return "not " + pred, nil
|
||||
}
|
||||
return "true", nil
|
||||
}
|
||||
|
||||
func (m *fieldMapper) ColumnExpressionFor(ctx context.Context, orgID valuer.UUID, tsStart, tsEnd uint64, field *telemetrytypes.TelemetryFieldKey, _ telemetrytypes.FieldDataType, _ map[string][]*telemetrytypes.TelemetryFieldKey) (string, error) {
|
||||
colName, err := m.FieldFor(ctx, orgID, tsStart, tsEnd, field)
|
||||
if err != nil {
|
||||
|
||||
@@ -274,15 +274,6 @@ func (store *store) SoftDeleteUser(ctx context.Context, orgID string, id string)
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete tokens")
|
||||
}
|
||||
|
||||
// delete user_role assignments so the roles can be deleted later
|
||||
_, err = tx.NewDelete().
|
||||
Model(new(authtypes.UserRole)).
|
||||
Where("user_id = ?", id).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete user roles")
|
||||
}
|
||||
|
||||
// soft delete user
|
||||
now := time.Now()
|
||||
_, err = tx.NewUpdate().
|
||||
|
||||
@@ -29,7 +29,6 @@ type builderQuery[T any] struct {
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
orgID valuer.UUID
|
||||
stmtBuilder qbtypes.StatementBuilder[T]
|
||||
queryType qbtypes.QueryType
|
||||
spec qbtypes.QueryBuilderQuery[T]
|
||||
variables map[string]qbtypes.VariableItem
|
||||
|
||||
@@ -52,7 +51,6 @@ func newBuilderQuery[T any](
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
orgID valuer.UUID,
|
||||
stmtBuilder qbtypes.StatementBuilder[T],
|
||||
queryType qbtypes.QueryType,
|
||||
spec qbtypes.QueryBuilderQuery[T],
|
||||
tr qbtypes.TimeRange,
|
||||
kind qbtypes.RequestType,
|
||||
@@ -64,7 +62,6 @@ func newBuilderQuery[T any](
|
||||
telemetryStore: telemetryStore,
|
||||
orgID: orgID,
|
||||
stmtBuilder: stmtBuilder,
|
||||
queryType: queryType,
|
||||
spec: spec,
|
||||
variables: variables,
|
||||
fromMS: tr.From,
|
||||
@@ -84,7 +81,7 @@ func (q *builderQuery[T]) Fingerprint() string {
|
||||
|
||||
// Create a deterministic fingerprint for builder queries
|
||||
// This needs to include all fields that affect the query results
|
||||
parts := []string{q.queryType.StringValue()}
|
||||
parts := []string{"builder"}
|
||||
|
||||
// Add signal type
|
||||
parts = append(parts, fmt.Sprintf("signal=%s", q.spec.Signal.StringValue()))
|
||||
|
||||
@@ -3,7 +3,6 @@ package querier
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
@@ -21,8 +20,7 @@ func TestBuilderQueryFingerprint(t *testing.T) {
|
||||
{
|
||||
name: "fingerprint includes shiftby when ShiftBy field is set",
|
||||
query: &builderQuery[qbtypes.MetricAggregation]{
|
||||
queryType: qbtypes.QueryTypeBuilder,
|
||||
kind: qbtypes.RequestTypeTimeSeries,
|
||||
kind: qbtypes.RequestTypeTimeSeries,
|
||||
spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
ShiftBy: 3600,
|
||||
@@ -42,8 +40,7 @@ func TestBuilderQueryFingerprint(t *testing.T) {
|
||||
{
|
||||
name: "fingerprint includes shiftby but not other functions",
|
||||
query: &builderQuery[qbtypes.MetricAggregation]{
|
||||
queryType: qbtypes.QueryTypeBuilder,
|
||||
kind: qbtypes.RequestTypeTimeSeries,
|
||||
kind: qbtypes.RequestTypeTimeSeries,
|
||||
spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
ShiftBy: 3600,
|
||||
@@ -66,8 +63,7 @@ func TestBuilderQueryFingerprint(t *testing.T) {
|
||||
{
|
||||
name: "no shiftby in fingerprint when ShiftBy is zero",
|
||||
query: &builderQuery[qbtypes.MetricAggregation]{
|
||||
queryType: qbtypes.QueryTypeBuilder,
|
||||
kind: qbtypes.RequestTypeTimeSeries,
|
||||
kind: qbtypes.RequestTypeTimeSeries,
|
||||
spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
ShiftBy: 0,
|
||||
@@ -98,29 +94,6 @@ func TestBuilderQueryFingerprint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderQueryFingerprintQueryType(t *testing.T) {
|
||||
spec := qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
|
||||
Aggregations: []qbtypes.TraceAggregation{{Expression: "count()"}},
|
||||
Filter: &qbtypes.Filter{Expression: "gen_ai.request.model EXISTS"},
|
||||
}
|
||||
regular := &builderQuery[qbtypes.TraceAggregation]{
|
||||
queryType: qbtypes.QueryTypeBuilder,
|
||||
kind: qbtypes.RequestTypeTimeSeries,
|
||||
spec: spec,
|
||||
}
|
||||
ai := &builderQuery[qbtypes.TraceAggregation]{
|
||||
queryType: qbtypes.QueryTypeBuilderAI,
|
||||
kind: qbtypes.RequestTypeTimeSeries,
|
||||
spec: spec,
|
||||
}
|
||||
|
||||
assert.True(t, strings.HasPrefix(regular.Fingerprint(), qbtypes.QueryTypeBuilder.StringValue()+"&"))
|
||||
assert.True(t, strings.HasPrefix(ai.Fingerprint(), qbtypes.QueryTypeBuilderAI.StringValue()+"&"))
|
||||
assert.NotEqual(t, regular.Fingerprint(), ai.Fingerprint())
|
||||
}
|
||||
|
||||
func TestMakeBucketsOrder(t *testing.T) {
|
||||
// Test that makeBuckets returns buckets in reverse chronological order by default
|
||||
// Using milliseconds as input - need > 1 hour range to get multiple buckets
|
||||
|
||||
@@ -305,7 +305,7 @@ func (q *querier) buildQueries(
|
||||
}
|
||||
spec.ShiftBy = extractShiftFromBuilderQuery(spec)
|
||||
timeRange := adjustTimeRangeForShift(spec, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType)
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, orgID, q.aiTraceStmtBuilder, query.Type, spec, timeRange, req.RequestType, tmplVars, builderConfig{})
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, orgID, q.aiTraceStmtBuilder, spec, timeRange, req.RequestType, tmplVars, builderConfig{})
|
||||
queries[spec.Name] = bq
|
||||
steps[spec.Name] = spec.StepInterval
|
||||
case qbtypes.QueryTypeBuilder:
|
||||
@@ -313,7 +313,7 @@ func (q *querier) buildQueries(
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
|
||||
spec.ShiftBy = extractShiftFromBuilderQuery(spec)
|
||||
timeRange := adjustTimeRangeForShift(spec, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType)
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, orgID, q.traceStmtBuilder, query.Type, spec, timeRange, req.RequestType, tmplVars, builderConfig{})
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, orgID, q.traceStmtBuilder, spec, timeRange, req.RequestType, tmplVars, builderConfig{})
|
||||
queries[spec.Name] = bq
|
||||
steps[spec.Name] = spec.StepInterval
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]:
|
||||
@@ -323,7 +323,7 @@ func (q *querier) buildQueries(
|
||||
if spec.Source == telemetrytypes.SourceAudit {
|
||||
stmtBuilder = q.auditStmtBuilder
|
||||
}
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, orgID, stmtBuilder, query.Type, spec, timeRange, req.RequestType, tmplVars, q.builderConfig)
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, orgID, stmtBuilder, spec, timeRange, req.RequestType, tmplVars, q.builderConfig)
|
||||
queries[spec.Name] = bq
|
||||
steps[spec.Name] = spec.StepInterval
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
|
||||
@@ -340,9 +340,9 @@ func (q *querier) buildQueries(
|
||||
|
||||
if spec.Source == telemetrytypes.SourceMeter {
|
||||
event.Source = telemetrytypes.SourceMeter.StringValue()
|
||||
bq = newBuilderQuery(q.logger, q.telemetryStore, orgID, q.meterStmtBuilder, query.Type, spec, timeRange, req.RequestType, tmplVars, builderConfig{})
|
||||
bq = newBuilderQuery(q.logger, q.telemetryStore, orgID, q.meterStmtBuilder, spec, timeRange, req.RequestType, tmplVars, builderConfig{})
|
||||
} else {
|
||||
bq = newBuilderQuery(q.logger, q.telemetryStore, orgID, q.metricStmtBuilder, query.Type, spec, timeRange, req.RequestType, tmplVars, builderConfig{})
|
||||
bq = newBuilderQuery(q.logger, q.telemetryStore, orgID, q.metricStmtBuilder, spec, timeRange, req.RequestType, tmplVars, builderConfig{})
|
||||
}
|
||||
|
||||
queries[spec.Name] = bq
|
||||
@@ -618,7 +618,7 @@ func (q *querier) QueryRawStream(ctx context.Context, orgID valuer.UUID, req *qb
|
||||
if spec.Source == telemetrytypes.SourceAudit {
|
||||
liveTailStmtBuilder = q.auditStmtBuilder
|
||||
}
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, orgID, liveTailStmtBuilder, query.Type, spec, timeRange, req.RequestType, map[string]qbtypes.VariableItem{
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, orgID, liveTailStmtBuilder, spec, timeRange, req.RequestType, map[string]qbtypes.VariableItem{
|
||||
"id": {
|
||||
Value: updatedLogID,
|
||||
},
|
||||
@@ -941,9 +941,8 @@ func (q *querier) createRangedQuery(_ valuer.UUID, originalQuery qbtypes.Query,
|
||||
specCopy := qt.spec.Copy()
|
||||
specCopy.ShiftBy = extractShiftFromBuilderQuery(specCopy)
|
||||
adjustedTimeRange := adjustTimeRangeForShift(specCopy, timeRange, qt.kind)
|
||||
// reuse the original query's statement builder and type so an AI query
|
||||
// keeps its AI builder and cache key
|
||||
return newBuilderQuery(q.logger, q.telemetryStore, qt.orgID, qt.stmtBuilder, qt.queryType, specCopy, adjustedTimeRange, qt.kind, qt.variables, qt.builderConfig)
|
||||
// reuse the original query's statement builder so an AI query keeps its AI builder
|
||||
return newBuilderQuery(q.logger, q.telemetryStore, qt.orgID, qt.stmtBuilder, specCopy, adjustedTimeRange, qt.kind, qt.variables, builderConfig{})
|
||||
|
||||
case *builderQuery[qbtypes.LogAggregation]:
|
||||
specCopy := qt.spec.Copy()
|
||||
@@ -953,16 +952,16 @@ func (q *querier) createRangedQuery(_ valuer.UUID, originalQuery qbtypes.Query,
|
||||
if qt.spec.Source == telemetrytypes.SourceAudit {
|
||||
shiftStmtBuilder = q.auditStmtBuilder
|
||||
}
|
||||
return newBuilderQuery(q.logger, q.telemetryStore, qt.orgID, shiftStmtBuilder, qt.queryType, specCopy, adjustedTimeRange, qt.kind, qt.variables, q.builderConfig)
|
||||
return newBuilderQuery(q.logger, q.telemetryStore, qt.orgID, shiftStmtBuilder, specCopy, adjustedTimeRange, qt.kind, qt.variables, q.builderConfig)
|
||||
|
||||
case *builderQuery[qbtypes.MetricAggregation]:
|
||||
specCopy := qt.spec.Copy()
|
||||
specCopy.ShiftBy = extractShiftFromBuilderQuery(specCopy)
|
||||
adjustedTimeRange := adjustTimeRangeForShift(specCopy, timeRange, qt.kind)
|
||||
if qt.spec.Source == telemetrytypes.SourceMeter {
|
||||
return newBuilderQuery(q.logger, q.telemetryStore, qt.orgID, q.meterStmtBuilder, qt.queryType, specCopy, adjustedTimeRange, qt.kind, qt.variables, builderConfig{})
|
||||
return newBuilderQuery(q.logger, q.telemetryStore, qt.orgID, q.meterStmtBuilder, specCopy, adjustedTimeRange, qt.kind, qt.variables, builderConfig{})
|
||||
}
|
||||
return newBuilderQuery(q.logger, q.telemetryStore, qt.orgID, q.metricStmtBuilder, qt.queryType, specCopy, adjustedTimeRange, qt.kind, qt.variables, builderConfig{})
|
||||
return newBuilderQuery(q.logger, q.telemetryStore, qt.orgID, q.metricStmtBuilder, specCopy, adjustedTimeRange, qt.kind, qt.variables, builderConfig{})
|
||||
case *traceOperatorQuery:
|
||||
specCopy := qt.spec.Copy()
|
||||
return &traceOperatorQuery{
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package querybuilder
|
||||
|
||||
import "strings"
|
||||
|
||||
// ClickHouseStringLiteral quotes a value for a ClickHouse string literal.
|
||||
func ClickHouseStringLiteral(value string) string {
|
||||
escaped := strings.ReplaceAll(value, `\`, `\\`)
|
||||
escaped = strings.ReplaceAll(escaped, `'`, `\'`)
|
||||
return "'" + escaped + "'"
|
||||
}
|
||||
|
||||
// ClickHouseIdentifier quotes a value for a ClickHouse identifier.
|
||||
func ClickHouseIdentifier(value string) string {
|
||||
escaped := strings.ReplaceAll(value, `\`, `\\`)
|
||||
escaped = strings.ReplaceAll(escaped, "`", "\\`")
|
||||
return "`" + escaped + "`"
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestClickHouseQuoting(t *testing.T) {
|
||||
t.Run("string literal", func(t *testing.T) {
|
||||
assert.Equal(t, `'name\'\\); SELECT 1 --'`, ClickHouseStringLiteral(`name'\); SELECT 1 --`))
|
||||
})
|
||||
|
||||
t.Run("identifier", func(t *testing.T) {
|
||||
assert.Equal(t, "`name\\`\\\\); SELECT 1 --`", ClickHouseIdentifier("name`\\); SELECT 1 --"))
|
||||
})
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/semconv"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// semconvFamiliesEnabled evaluates the resolve_semconv_families flag for the
|
||||
// org. A nil flagger means off, so a caller without family support stays
|
||||
// literal by default.
|
||||
func semconvFamiliesEnabled(ctx context.Context, orgID valuer.UUID, fl flagger.Flagger) bool {
|
||||
if fl == nil {
|
||||
return false
|
||||
}
|
||||
return fl.BooleanOrEmpty(ctx, flagger.FeatureResolveSemconvFamilies, featuretypes.NewFlaggerEvaluationContext(orgID))
|
||||
}
|
||||
|
||||
// ExpandKeySelectorsForFamilies adds selectors for the other members of each
|
||||
// semantic-convention family that a selector names. The metadata fetched for
|
||||
// a query then contains each spelling that MatchingLogicalFields can group.
|
||||
// This function is the prefetch of the resolution layer: statement builders
|
||||
// call it after they derive the selectors, and the metadata store stays
|
||||
// family-blind (autocomplete responses keep the literal spelling that the
|
||||
// user typed). It does nothing when the resolve_semconv_families flag is off
|
||||
// for the org. Only trace selectors expand today, because that matches the
|
||||
// family support. Fuzzy (search-style) selectors never expand.
|
||||
func ExpandKeySelectorsForFamilies(ctx context.Context, orgID valuer.UUID, fl flagger.Flagger, selectors []*telemetrytypes.FieldKeySelector) []*telemetrytypes.FieldKeySelector {
|
||||
if !semconvFamiliesEnabled(ctx, orgID, fl) {
|
||||
return selectors
|
||||
}
|
||||
|
||||
out := selectors
|
||||
seen := make(map[string]bool, len(selectors))
|
||||
for _, selector := range selectors {
|
||||
seen[selector.Name] = true
|
||||
}
|
||||
|
||||
for _, selector := range selectors {
|
||||
if selector.Signal != telemetrytypes.SignalTraces ||
|
||||
selector.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeFuzzy {
|
||||
continue
|
||||
}
|
||||
members := semconv.Members(semconv.KindAttribute, telemetrytypes.FieldKeySelector{
|
||||
Name: selector.Name,
|
||||
Signal: selector.Signal,
|
||||
FieldContext: selector.FieldContext,
|
||||
})
|
||||
for _, member := range members {
|
||||
if seen[member] {
|
||||
continue
|
||||
}
|
||||
seen[member] = true
|
||||
expanded := *selector
|
||||
expanded.Name = member
|
||||
out = append(out, &expanded)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -21,25 +21,24 @@ const (
|
||||
hasTokenFunctionDocURL = "https://signoz.io/docs/userguide/functions-reference/#hastoken-function"
|
||||
)
|
||||
|
||||
// ResolveLogicalFields picks which logical fields a filter term builds conditions
|
||||
// for. With 0 or 1 field it returns the input unchanged and no warning. When a
|
||||
// name is ambiguous (several logical fields — a family is one field and never
|
||||
// ambiguous with itself) it returns a warning; a resource+attribute mix defaults
|
||||
// to the resource fields (the common intent), noted in the warning.
|
||||
func ResolveLogicalFields(field *telemetrytypes.TelemetryFieldKey, logicalFields []*telemetrytypes.LogicalField) ([]*telemetrytypes.LogicalField, string) {
|
||||
if len(logicalFields) <= 1 {
|
||||
return logicalFields, ""
|
||||
// ResolveKeys picks which matching field keys a filter term builds conditions for.
|
||||
// With 0 or 1 match it returns the input unchanged and no warning. When a name is
|
||||
// ambiguous it returns a warning; a resource+attribute mix defaults to the resource
|
||||
// keys (the common intent), noted in the warning.
|
||||
func ResolveKeys(field *telemetrytypes.TelemetryFieldKey, fieldKeysForName []*telemetrytypes.TelemetryFieldKey) ([]*telemetrytypes.TelemetryFieldKey, string) {
|
||||
if len(fieldKeysForName) <= 1 {
|
||||
return fieldKeysForName, ""
|
||||
}
|
||||
|
||||
warning := fmt.Sprintf(
|
||||
"Key `%s` is ambiguous, found %d different combinations of field context / data type: %v.",
|
||||
field.Name,
|
||||
len(logicalFields),
|
||||
logicalFields,
|
||||
len(fieldKeysForName),
|
||||
fieldKeysForName,
|
||||
)
|
||||
|
||||
hasResource, hasAttribute := false, false
|
||||
for _, item := range logicalFields {
|
||||
for _, item := range fieldKeysForName {
|
||||
switch item.FieldContext {
|
||||
case telemetrytypes.FieldContextResource:
|
||||
hasResource = true
|
||||
@@ -50,40 +49,18 @@ func ResolveLogicalFields(field *telemetrytypes.TelemetryFieldKey, logicalFields
|
||||
|
||||
// when there is both resource and attribute context, default to resource only
|
||||
if hasResource && hasAttribute {
|
||||
filtered := make([]*telemetrytypes.LogicalField, 0, len(logicalFields))
|
||||
for _, item := range logicalFields {
|
||||
filteredKeys := make([]*telemetrytypes.TelemetryFieldKey, 0, len(fieldKeysForName))
|
||||
for _, item := range fieldKeysForName {
|
||||
if item.FieldContext == telemetrytypes.FieldContextResource {
|
||||
filtered = append(filtered, item)
|
||||
filteredKeys = append(filteredKeys, item)
|
||||
}
|
||||
}
|
||||
logicalFields = filtered
|
||||
fieldKeysForName = filteredKeys
|
||||
warning += " " + "Using `resource` context by default. To query attributes explicitly, " +
|
||||
fmt.Sprintf("use the fully qualified name (e.g., 'attribute.%s')", field.Name)
|
||||
}
|
||||
|
||||
return logicalFields, warning
|
||||
}
|
||||
|
||||
// WrapAsLogicalFields wraps physical keys (candidate or synthesized) as
|
||||
// single-member logical fields addressed by the requested spelling.
|
||||
func WrapAsLogicalFields(requestedName string, keys []*telemetrytypes.TelemetryFieldKey) []*telemetrytypes.LogicalField {
|
||||
fields := make([]*telemetrytypes.LogicalField, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
fields = append(fields, telemetrytypes.SingleLogicalField(requestedName, key))
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// SingleKeys flattens logical fields to their single members. It is the
|
||||
// adapter for signals whose fields are single-member by construction (every
|
||||
// signal without family support); their condition builders keep compiling per
|
||||
// physical key.
|
||||
func SingleKeys(fields []*telemetrytypes.LogicalField) []*telemetrytypes.TelemetryFieldKey {
|
||||
keys := make([]*telemetrytypes.TelemetryFieldKey, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
keys = append(keys, field.Single())
|
||||
}
|
||||
return keys
|
||||
return fieldKeysForName, warning
|
||||
}
|
||||
|
||||
// NewKeyNotFoundError builds the error a condition builder returns when a filter term
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// The two functions below are the only place family expressions are built.
|
||||
// They compose exclusively from the mapper's per-key primitives (FieldFor,
|
||||
// ExistsFor), so every member honors its own storage: materialized columns,
|
||||
// evolutions, and JSON plans ride the member keys, and a signal supports
|
||||
// families the moment its primitives are correct.
|
||||
|
||||
// LogicalValueExpr returns the value expression for a resolved logical field:
|
||||
// the member's own expression for a single-member field, and a current-first
|
||||
// merge across the members' expressions for a family.
|
||||
func LogicalValueExpr(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
tsStart, tsEnd uint64,
|
||||
fm qbtypes.FieldMapper,
|
||||
logical *telemetrytypes.LogicalField,
|
||||
) (string, error) {
|
||||
if !logical.IsFamily() {
|
||||
return fm.FieldFor(ctx, orgID, tsStart, tsEnd, logical.Single())
|
||||
}
|
||||
|
||||
memberExprs := make([]string, 0, len(logical.Members))
|
||||
for _, member := range logical.Members {
|
||||
expr, err := fm.FieldFor(ctx, orgID, tsStart, tsEnd, member)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
memberExprs = append(memberExprs, expr)
|
||||
}
|
||||
|
||||
if logical.FieldDataType == telemetrytypes.FieldDataTypeString {
|
||||
// The trailing '' keeps single-key semantics for rows without any
|
||||
// member: string maps read '' for an absent key, and negative
|
||||
// operators must keep including such rows (see AddDefaultExistsFilter).
|
||||
// A NULL tail would drop them: NULL != 'x' evaluates to NULL, and the
|
||||
// row falls out of the result.
|
||||
values := make([]string, 0, len(memberExprs))
|
||||
for _, expr := range memberExprs {
|
||||
values = append(values, fmt.Sprintf("NULLIF(%s, '')", expr))
|
||||
}
|
||||
return "COALESCE(" + strings.Join(values, ", ") + ", '')", nil
|
||||
}
|
||||
|
||||
// Numeric and boolean maps return zero for an absent key. If a family of
|
||||
// either type is enabled, this tail must become zero too.
|
||||
branches := make([]string, 0, len(logical.Members)*2)
|
||||
for i, member := range logical.Members {
|
||||
guard, err := fm.ExistsFor(ctx, orgID, tsStart, tsEnd, member, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
branches = append(branches, guard, memberExprs[i])
|
||||
}
|
||||
return "multiIf(" + strings.Join(branches, ", ") + ", NULL)", nil
|
||||
}
|
||||
|
||||
// LogicalExistsExpr returns the existence predicate for a resolved logical
|
||||
// field: the member's own predicate for a single-member field, presence of
|
||||
// any member for a family.
|
||||
func LogicalExistsExpr(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
tsStart, tsEnd uint64,
|
||||
fm qbtypes.FieldMapper,
|
||||
logical *telemetrytypes.LogicalField,
|
||||
exists bool,
|
||||
) (string, error) {
|
||||
if !logical.IsFamily() {
|
||||
return fm.ExistsFor(ctx, orgID, tsStart, tsEnd, logical.Single(), exists)
|
||||
}
|
||||
|
||||
guards := make([]string, 0, len(logical.Members))
|
||||
for _, member := range logical.Members {
|
||||
guard, err := fm.ExistsFor(ctx, orgID, tsStart, tsEnd, member, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
guards = append(guards, guard)
|
||||
}
|
||||
combined := "(" + strings.Join(guards, " OR ") + ")"
|
||||
if exists {
|
||||
return combined, nil
|
||||
}
|
||||
return "NOT " + combined, nil
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// stubFieldMapper provides just the two per-key primitives the shared
|
||||
// composition builds on; the remaining FieldMapper methods are unused here.
|
||||
type stubFieldMapper struct{}
|
||||
|
||||
func (stubFieldMapper) FieldFor(_ context.Context, _ valuer.UUID, _, _ uint64, key *telemetrytypes.TelemetryFieldKey) (string, error) {
|
||||
return "value(" + key.Name + ")", nil
|
||||
}
|
||||
|
||||
func (stubFieldMapper) ExistsFor(_ context.Context, _ valuer.UUID, _, _ uint64, key *telemetrytypes.TelemetryFieldKey, exists bool) (string, error) {
|
||||
if exists {
|
||||
return "has(" + key.Name + ")", nil
|
||||
}
|
||||
return "NOT has(" + key.Name + ")", nil
|
||||
}
|
||||
|
||||
func (stubFieldMapper) ColumnFor(context.Context, valuer.UUID, uint64, uint64, *telemetrytypes.TelemetryFieldKey) ([]*schema.Column, error) {
|
||||
return nil, qbtypes.ErrColumnNotFound
|
||||
}
|
||||
|
||||
func (stubFieldMapper) ColumnExpressionFor(context.Context, valuer.UUID, uint64, uint64, *telemetrytypes.TelemetryFieldKey, telemetrytypes.FieldDataType, map[string][]*telemetrytypes.TelemetryFieldKey) (string, error) {
|
||||
return "", qbtypes.ErrColumnNotFound
|
||||
}
|
||||
|
||||
func (stubFieldMapper) CandidateKeys(context.Context, valuer.UUID, *telemetrytypes.TelemetryFieldKey, any, map[string][]*telemetrytypes.TelemetryFieldKey) []*telemetrytypes.TelemetryFieldKey {
|
||||
return nil
|
||||
}
|
||||
|
||||
func stringFamily(names ...string) *telemetrytypes.LogicalField {
|
||||
members := make([]*telemetrytypes.TelemetryFieldKey, 0, len(names))
|
||||
for _, name := range names {
|
||||
members = append(members, &telemetrytypes.TelemetryFieldKey{Name: name, FieldDataType: telemetrytypes.FieldDataTypeString})
|
||||
}
|
||||
return &telemetrytypes.LogicalField{Name: names[0], FieldDataType: telemetrytypes.FieldDataTypeString, Members: members}
|
||||
}
|
||||
|
||||
func TestLogicalValueExprSingleMemberDelegatesToFieldFor(t *testing.T) {
|
||||
logical := telemetrytypes.SingleLogicalField("a", &telemetrytypes.TelemetryFieldKey{Name: "a"})
|
||||
expr, err := LogicalValueExpr(context.Background(), valuer.UUID{}, 0, 0, stubFieldMapper{}, logical)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "value(a)", expr)
|
||||
}
|
||||
|
||||
func TestLogicalValueExprStringFamilyMergesCurrentFirst(t *testing.T) {
|
||||
expr, err := LogicalValueExpr(context.Background(), valuer.UUID{}, 0, 0, stubFieldMapper{}, stringFamily("current", "old"))
|
||||
require.NoError(t, err)
|
||||
// The trailing '' preserves keyless-row semantics for negative operators.
|
||||
assert.Equal(t, "COALESCE(NULLIF(value(current), ''), NULLIF(value(old), ''), '')", expr)
|
||||
}
|
||||
|
||||
func TestLogicalValueExprNumericFamilyGuardsEveryMember(t *testing.T) {
|
||||
logical := &telemetrytypes.LogicalField{
|
||||
Name: "current",
|
||||
FieldDataType: telemetrytypes.FieldDataTypeNumber,
|
||||
Members: []*telemetrytypes.TelemetryFieldKey{
|
||||
{Name: "current", FieldDataType: telemetrytypes.FieldDataTypeNumber},
|
||||
{Name: "old", FieldDataType: telemetrytypes.FieldDataTypeNumber},
|
||||
},
|
||||
}
|
||||
expr, err := LogicalValueExpr(context.Background(), valuer.UUID{}, 0, 0, stubFieldMapper{}, logical)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "multiIf(has(current), value(current), has(old), value(old), NULL)", expr)
|
||||
}
|
||||
|
||||
func TestLogicalExistsExprSingleMemberDelegatesToExistsFor(t *testing.T) {
|
||||
logical := telemetrytypes.SingleLogicalField("a", &telemetrytypes.TelemetryFieldKey{Name: "a"})
|
||||
expr, err := LogicalExistsExpr(context.Background(), valuer.UUID{}, 0, 0, stubFieldMapper{}, logical, false)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "NOT has(a)", expr)
|
||||
}
|
||||
|
||||
func TestLogicalExistsExprFamilyIsAnyMemberPresence(t *testing.T) {
|
||||
family := stringFamily("current", "old")
|
||||
|
||||
expr, err := LogicalExistsExpr(context.Background(), valuer.UUID{}, 0, 0, stubFieldMapper{}, family, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "(has(current) OR has(old))", expr)
|
||||
|
||||
expr, err = LogicalExistsExpr(context.Background(), valuer.UUID{}, 0, 0, stubFieldMapper{}, family, false)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "NOT (has(current) OR has(old))", expr)
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// familiesOn returns a flagger with resolve_semconv_families on.
|
||||
func familiesOn(t *testing.T) flagger.Flagger {
|
||||
return flaggertest.WithBooleanFlags(t, map[string]bool{
|
||||
flagger.FeatureResolveSemconvFamilies.String(): true,
|
||||
})
|
||||
}
|
||||
|
||||
func memberNames(logical *telemetrytypes.LogicalField) []string {
|
||||
names := make([]string, 0, len(logical.Members))
|
||||
for _, member := range logical.Members {
|
||||
names = append(names, member.Name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// The deployment.environment(.name) family (enabled in pkg/semconv) drives the
|
||||
// grouping tests below.
|
||||
|
||||
// With the resolve_semconv_families flag off, matches stay single-member and
|
||||
// selectors stay literal, even when the metadata map has both spellings.
|
||||
func TestFamiliesOffByDefault(t *testing.T) {
|
||||
fieldKeys := map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"deployment.environment.name": {{
|
||||
Name: "deployment.environment.name",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
"deployment.environment": {{
|
||||
Name: "deployment.environment",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
}
|
||||
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, flaggertest.New(t), &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"}, fieldKeys)
|
||||
require.Len(t, fields, 1)
|
||||
assert.False(t, fields[0].IsFamily())
|
||||
assert.Equal(t, []string{"deployment.environment.name"}, memberNames(fields[0]))
|
||||
|
||||
selectors := []*telemetrytypes.FieldKeySelector{
|
||||
{Name: "deployment.environment.name", Signal: telemetrytypes.SignalTraces, SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeExact},
|
||||
}
|
||||
assert.Len(t, ExpandKeySelectorsForFamilies(context.Background(), valuer.UUID{}, flaggertest.New(t), selectors), 1)
|
||||
}
|
||||
|
||||
func TestMatchingLogicalFieldsGroupsFamilyMembers(t *testing.T) {
|
||||
fieldKeys := map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"deployment.environment.name": {{
|
||||
Name: "deployment.environment.name",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
"deployment.environment": {{
|
||||
Name: "deployment.environment",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
}
|
||||
|
||||
for _, requested := range []string{"deployment.environment.name", "deployment.environment"} {
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, familiesOn(t), &telemetrytypes.TelemetryFieldKey{Name: requested}, fieldKeys)
|
||||
require.Len(t, fields, 1, "a family is one logical field, requested via %s", requested)
|
||||
logical := fields[0]
|
||||
assert.Equal(t, requested, logical.Name, "response identity is the requested spelling")
|
||||
assert.Equal(t, telemetrytypes.FieldContextResource, logical.FieldContext)
|
||||
assert.True(t, logical.IsFamily())
|
||||
assert.Equal(t, []string{"deployment.environment.name", "deployment.environment"}, memberNames(logical),
|
||||
"members are current-first regardless of the requested spelling")
|
||||
}
|
||||
}
|
||||
|
||||
// Member precedence is the family's current-first order, not lookup arrival
|
||||
// order: a current-name key found only under its context-prefixed spelling
|
||||
// arrives in the second lookup pass yet must still sort first.
|
||||
func TestMatchingLogicalFieldsOrdersMembersByFamilyRank(t *testing.T) {
|
||||
fieldKeys := map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"deployment.environment": {{
|
||||
Name: "deployment.environment",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
"resource.deployment.environment.name": {{
|
||||
Name: "resource.deployment.environment.name",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
}
|
||||
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, familiesOn(t), &telemetrytypes.TelemetryFieldKey{
|
||||
Name: "deployment.environment.name",
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
}, fieldKeys)
|
||||
|
||||
require.Len(t, fields, 1)
|
||||
assert.Equal(t, []string{"resource.deployment.environment.name", "deployment.environment"}, memberNames(fields[0]))
|
||||
}
|
||||
|
||||
// Non-trace signals have no family support: the requested spelling stays
|
||||
// literal, and a family member name never pulls in its siblings.
|
||||
func TestMatchingLogicalFieldsKeepsLogsLiteral(t *testing.T) {
|
||||
logsKey := func(name string) *telemetrytypes.TelemetryFieldKey {
|
||||
return &telemetrytypes.TelemetryFieldKey{
|
||||
Name: name,
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}
|
||||
}
|
||||
fieldKeys := map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"deployment.environment.name": {logsKey("deployment.environment.name")},
|
||||
"deployment.environment": {logsKey("deployment.environment")},
|
||||
}
|
||||
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, familiesOn(t), &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"}, fieldKeys)
|
||||
require.Len(t, fields, 1)
|
||||
assert.False(t, fields[0].IsFamily())
|
||||
assert.Equal(t, []string{"deployment.environment.name"}, memberNames(fields[0]))
|
||||
}
|
||||
|
||||
// A family and a genuine same-name collision stack cleanly: the family stays
|
||||
// one logical field, the collision adds another, and resource preference keeps
|
||||
// the family as a unit.
|
||||
func TestResolveLogicalFieldsKeepsFamilyThroughAmbiguity(t *testing.T) {
|
||||
fieldKeys := map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"deployment.environment.name": {
|
||||
{
|
||||
Name: "deployment.environment.name",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
{
|
||||
Name: "deployment.environment.name",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
},
|
||||
"deployment.environment": {{
|
||||
Name: "deployment.environment",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
}
|
||||
|
||||
requested := &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"}
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, familiesOn(t), requested, fieldKeys)
|
||||
require.Len(t, fields, 2, "resource family + attribute collision")
|
||||
|
||||
resolved, warning := ResolveLogicalFields(requested, fields)
|
||||
assert.NotEmpty(t, warning)
|
||||
require.Len(t, resolved, 1)
|
||||
assert.Equal(t, telemetrytypes.FieldContextResource, resolved[0].FieldContext)
|
||||
assert.Equal(t, []string{"deployment.environment.name", "deployment.environment"}, memberNames(resolved[0]))
|
||||
}
|
||||
|
||||
// Members of a family with different data types never merge: the identity
|
||||
// (signal, context, data type) separates them into distinct logical fields.
|
||||
func TestMatchingLogicalFieldsNeverMergesAcrossDataTypes(t *testing.T) {
|
||||
fieldKeys := map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"deployment.environment.name": {{
|
||||
Name: "deployment.environment.name",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
"deployment.environment": {{
|
||||
Name: "deployment.environment",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeNumber,
|
||||
}},
|
||||
}
|
||||
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, familiesOn(t), &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"}, fieldKeys)
|
||||
require.Len(t, fields, 2)
|
||||
for _, logical := range fields {
|
||||
assert.False(t, logical.IsFamily())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandKeySelectorsForFamilies(t *testing.T) {
|
||||
selectors := []*telemetrytypes.FieldKeySelector{
|
||||
{Name: "deployment.environment.name", Signal: telemetrytypes.SignalTraces, SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeExact},
|
||||
{Name: "service.name", Signal: telemetrytypes.SignalTraces, SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeExact},
|
||||
{Name: "deployment.environment.name", Signal: telemetrytypes.SignalLogs, SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeExact},
|
||||
}
|
||||
|
||||
expanded := ExpandKeySelectorsForFamilies(context.Background(), valuer.UUID{}, familiesOn(t), selectors)
|
||||
|
||||
names := make([]string, 0, len(expanded))
|
||||
for _, selector := range expanded {
|
||||
names = append(names, selector.Name)
|
||||
}
|
||||
assert.Equal(t, []string{
|
||||
"deployment.environment.name",
|
||||
"service.name",
|
||||
"deployment.environment.name",
|
||||
"deployment.environment",
|
||||
}, names, "one sibling selector for the trace family member; logs and non-family names untouched")
|
||||
|
||||
sibling := expanded[len(expanded)-1]
|
||||
assert.Equal(t, telemetrytypes.SignalTraces, sibling.Signal)
|
||||
assert.Equal(t, telemetrytypes.FieldSelectorMatchTypeExact, sibling.SelectorMatchType)
|
||||
}
|
||||
|
||||
func TestExpandKeySelectorsForFamiliesDeduplicatesAndSkipsFuzzy(t *testing.T) {
|
||||
both := []*telemetrytypes.FieldKeySelector{
|
||||
{Name: "deployment.environment.name", Signal: telemetrytypes.SignalTraces, SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeExact},
|
||||
{Name: "deployment.environment", Signal: telemetrytypes.SignalTraces, SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeExact},
|
||||
}
|
||||
assert.Len(t, ExpandKeySelectorsForFamilies(context.Background(), valuer.UUID{}, familiesOn(t), both), 2, "both spellings already referenced")
|
||||
|
||||
fuzzy := []*telemetrytypes.FieldKeySelector{
|
||||
{Name: "deployment.environment.name", Signal: telemetrytypes.SignalTraces, SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy},
|
||||
}
|
||||
assert.Len(t, ExpandKeySelectorsForFamilies(context.Background(), valuer.UUID{}, familiesOn(t), fuzzy), 1, "fuzzy (search-style) selectors never expand")
|
||||
}
|
||||
@@ -9,9 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/filterquery/grammar"
|
||||
"github.com/SigNoz/signoz/pkg/semconv"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -29,7 +27,6 @@ const stringMatchingOperatorDocURL = "https://signoz.io/docs/userguide/operators
|
||||
type filterExpressionVisitor struct {
|
||||
context context.Context
|
||||
orgID valuer.UUID
|
||||
fl flagger.Flagger
|
||||
fieldMapper qbtypes.FieldMapper
|
||||
conditionBuilder qbtypes.ConditionBuilder
|
||||
warnings []string
|
||||
@@ -51,11 +48,8 @@ type filterExpressionVisitor struct {
|
||||
}
|
||||
|
||||
type FilterExprVisitorOpts struct {
|
||||
Context context.Context
|
||||
OrgID valuer.UUID
|
||||
// Flagger evaluates the resolve_semconv_families flag during resolution.
|
||||
// A nil Flagger keeps resolution literal.
|
||||
Flagger flagger.Flagger
|
||||
Context context.Context
|
||||
OrgID valuer.UUID
|
||||
Logger *slog.Logger
|
||||
FieldMapper qbtypes.FieldMapper
|
||||
ConditionBuilder qbtypes.ConditionBuilder
|
||||
@@ -74,7 +68,6 @@ func newFilterExpressionVisitor(opts FilterExprVisitorOpts) *filterExpressionVis
|
||||
return &filterExpressionVisitor{
|
||||
context: opts.Context,
|
||||
orgID: opts.OrgID,
|
||||
fl: opts.Flagger,
|
||||
fieldMapper: opts.FieldMapper,
|
||||
conditionBuilder: opts.ConditionBuilder,
|
||||
fieldKeys: opts.FieldKeys,
|
||||
@@ -367,7 +360,7 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
}
|
||||
conds, ok := v.buildConditions(v.fullTextColumn, []*telemetrytypes.LogicalField{telemetrytypes.SingleLogicalField(v.fullTextColumn.Name, v.fullTextColumn)}, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(searchText))
|
||||
conds, ok := v.buildConditions(v.fullTextColumn, []*telemetrytypes.TelemetryFieldKey{v.fullTextColumn}, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(searchText))
|
||||
if !ok {
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
@@ -386,7 +379,7 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
|
||||
// VisitComparison handles all comparison operators.
|
||||
func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext) any {
|
||||
key := v.Visit(ctx.Key()).(*telemetrytypes.TelemetryFieldKey)
|
||||
matching := MatchingLogicalFields(v.context, v.orgID, v.fl, key, v.fieldKeys)
|
||||
matching := MatchingFieldKeys(key, v.fieldKeys)
|
||||
|
||||
// Handle EXISTS specially
|
||||
if ctx.EXISTS() != nil {
|
||||
@@ -682,7 +675,7 @@ func (v *filterExpressionVisitor) VisitFullText(ctx *grammar.FullTextContext) an
|
||||
v.errors = append(v.errors, "full text search is not supported")
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
conds, ok := v.buildConditions(v.fullTextColumn, []*telemetrytypes.LogicalField{telemetrytypes.SingleLogicalField(v.fullTextColumn.Name, v.fullTextColumn)}, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(text))
|
||||
conds, ok := v.buildConditions(v.fullTextColumn, []*telemetrytypes.TelemetryFieldKey{v.fullTextColumn}, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(text))
|
||||
if !ok {
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
@@ -737,7 +730,7 @@ func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallCon
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
|
||||
conds, ok := v.buildConditions(key, MatchingLogicalFields(v.context, v.orgID, v.fl, key, v.fieldKeys), operator, value)
|
||||
conds, ok := v.buildConditions(key, MatchingFieldKeys(key, v.fieldKeys), operator, value)
|
||||
if !ok {
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
@@ -929,7 +922,7 @@ func (v *filterExpressionVisitor) VisitKey(ctx *grammar.KeyContext) any {
|
||||
|
||||
// buildConditions invokes the condition builder for a filter term, folding its
|
||||
// warnings/errors into visitor state; returns false if an error was recorded.
|
||||
func (v *filterExpressionVisitor) buildConditions(key *telemetrytypes.TelemetryFieldKey, matching []*telemetrytypes.LogicalField, op qbtypes.FilterOperator, value any) ([]string, bool) {
|
||||
func (v *filterExpressionVisitor) buildConditions(key *telemetrytypes.TelemetryFieldKey, matching []*telemetrytypes.TelemetryFieldKey, op qbtypes.FilterOperator, value any) ([]string, bool) {
|
||||
conds, warns, err := v.conditionBuilder.ConditionFor(v.context, v.orgID, v.startNs, v.endNs, key, v.fieldKeys, qbtypes.ConditionBuilderOptions{SkipResourceFilter: v.skipResourceFilter}, op, value, v.builder)
|
||||
if err != nil {
|
||||
_, _, _, _, errURL, _ := errors.Unwrapb(err)
|
||||
@@ -986,161 +979,30 @@ func assignIfEmpty(s *string, value string) {
|
||||
}
|
||||
}
|
||||
|
||||
// familyMemberNames returns the physical spellings to look up for the
|
||||
// referenced key: the semantic-convention family members (current-first) when
|
||||
// the resolve_semconv_families flag is on for the org and the key can resolve
|
||||
// to traces, else just the requested name. Only trace field mappers understand
|
||||
// families today; logs and metrics keep the requested spelling until theirs
|
||||
// land.
|
||||
func familyMemberNames(ctx context.Context, orgID valuer.UUID, fl flagger.Flagger, field *telemetrytypes.TelemetryFieldKey) []string {
|
||||
if !semconvFamiliesEnabled(ctx, orgID, fl) {
|
||||
return []string{field.Name}
|
||||
}
|
||||
if field.Signal != telemetrytypes.SignalUnspecified && field.Signal != telemetrytypes.SignalTraces {
|
||||
return []string{field.Name}
|
||||
}
|
||||
return semconv.Members(semconv.KindAttribute, telemetrytypes.FieldKeySelector{
|
||||
Name: field.Name,
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: field.FieldContext,
|
||||
})
|
||||
}
|
||||
// MatchingFieldKeys returns the field keys from the map that match the given key,
|
||||
// honoring any context/data type the user specified.
|
||||
func MatchingFieldKeys(field *telemetrytypes.TelemetryFieldKey, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey) []*telemetrytypes.TelemetryFieldKey {
|
||||
fieldKeysForName := []*telemetrytypes.TelemetryFieldKey{}
|
||||
|
||||
// MatchingLogicalFields resolves the referenced key against the metadata map
|
||||
// into logical fields, honoring any context/data type the user specified.
|
||||
//
|
||||
// Physical keys that are members of one semantic-convention family (traces
|
||||
// only today) group into one logical field per (signal, context, data type)
|
||||
// identity, members ordered current-first. Every other matching key becomes
|
||||
// its own single-member logical field. Ambiguity is the length of the
|
||||
// returned slice: one family is one element and is never ambiguous with
|
||||
// itself, but the slice can hold several logical fields — including several
|
||||
// family fields, one per identity, when the family exists under more than
|
||||
// one context or data type. Members alias the metadata map entries; nothing
|
||||
// is copied or mutated.
|
||||
//
|
||||
// Family grouping only happens when the resolve_semconv_families flag is on
|
||||
// for the org. A nil flagger means off: every match then stays a
|
||||
// single-member logical field.
|
||||
func MatchingLogicalFields(ctx context.Context, orgID valuer.UUID, fl flagger.Flagger, field *telemetrytypes.TelemetryFieldKey, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey) []*telemetrytypes.LogicalField {
|
||||
members := familyMemberNames(ctx, orgID, fl, field)
|
||||
matches := collectMemberMatches(field, members, fieldKeys)
|
||||
return groupIntoLogicalFields(field.Name, len(members) > 1, matches)
|
||||
}
|
||||
|
||||
// memberMatch pairs a metadata entry with the family rank of the member name
|
||||
// it matched under. The stored name of a context-prefixed match differs from
|
||||
// the member name, so the rank must travel with the match.
|
||||
type memberMatch struct {
|
||||
key *telemetrytypes.TelemetryFieldKey
|
||||
rank int
|
||||
}
|
||||
|
||||
// matchesRequestedIdentity reports whether the entry fits the context and data
|
||||
// type that the request specified; unspecified matches any. A context-prefixed
|
||||
// lookup already matched the context through the lookup key itself.
|
||||
func matchesRequestedIdentity(field, item *telemetrytypes.TelemetryFieldKey, contextMatched bool) bool {
|
||||
if !contextMatched && field.FieldContext != telemetrytypes.FieldContextUnspecified && field.FieldContext != item.FieldContext {
|
||||
return false
|
||||
}
|
||||
if field.FieldDataType != telemetrytypes.FieldDataTypeUnspecified && field.FieldDataType != item.FieldDataType {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// inFamilyScope reports whether a match found under a sibling member name is
|
||||
// legitimate: the entry must be trace metadata, and the member must be in the
|
||||
// family of the requested name for the entry's context. A member lookup can
|
||||
// otherwise find a same-named field in a scope where the family does not
|
||||
// apply.
|
||||
func inFamilyScope(field, item *telemetrytypes.TelemetryFieldKey, memberName string) bool {
|
||||
if item.Signal != telemetrytypes.SignalTraces {
|
||||
return false
|
||||
}
|
||||
return slices.Contains(semconv.Members(semconv.KindAttribute, telemetrytypes.FieldKeySelector{
|
||||
Name: field.Name,
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: item.FieldContext,
|
||||
}), memberName)
|
||||
}
|
||||
|
||||
// collectMemberMatches finds the metadata entries for every member spelling:
|
||||
// first under the member names, then under their context-prefixed spellings
|
||||
// (a context can be a legitimate part of a stored name, e.g. `attribute.key`).
|
||||
func collectMemberMatches(field *telemetrytypes.TelemetryFieldKey, members []string, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey) []memberMatch {
|
||||
matches := make([]memberMatch, 0)
|
||||
collect := func(lookupName string, rank int, memberName string, contextMatched bool) {
|
||||
for _, item := range fieldKeys[lookupName] {
|
||||
if !matchesRequestedIdentity(field, item, contextMatched) {
|
||||
continue
|
||||
}
|
||||
if memberName != field.Name && !inFamilyScope(field, item, memberName) {
|
||||
continue
|
||||
}
|
||||
matches = append(matches, memberMatch{key: item, rank: rank})
|
||||
// match by name; keep items whose context and data type match (unspecified matches any)
|
||||
for _, item := range fieldKeys[field.Name] {
|
||||
if (field.FieldContext == telemetrytypes.FieldContextUnspecified || field.FieldContext == item.FieldContext) &&
|
||||
(field.FieldDataType == telemetrytypes.FieldDataTypeUnspecified || field.FieldDataType == item.FieldDataType) {
|
||||
fieldKeysForName = append(fieldKeysForName, item)
|
||||
}
|
||||
}
|
||||
|
||||
for rank, member := range members {
|
||||
collect(member, rank, member, false)
|
||||
}
|
||||
// A context may have been split off a name that legitimately contained it (e.g.
|
||||
// `attribute.key`); also look up the context-prefixed name so both readings resolve.
|
||||
if field.FieldContext != telemetrytypes.FieldContextUnspecified {
|
||||
for rank, member := range members {
|
||||
collect(fmt.Sprintf("%s.%s", field.FieldContext.StringValue(), member), rank, member, true)
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
// groupIntoLogicalFields turns matches into logical fields. Trace entries in
|
||||
// family mode group by their (signal, context, data type) identity; every
|
||||
// other entry becomes its own single-member field. Members sort by family
|
||||
// rank at the end: precedence is a property of the family, not of the order
|
||||
// in which the lookups found the members.
|
||||
func groupIntoLogicalFields(requestedName string, familyMode bool, matches []memberMatch) []*telemetrytypes.LogicalField {
|
||||
fields := make([]*telemetrytypes.LogicalField, 0, len(matches))
|
||||
groups := make(map[string]*telemetrytypes.LogicalField)
|
||||
ranks := make(map[*telemetrytypes.TelemetryFieldKey]int)
|
||||
|
||||
for _, match := range matches {
|
||||
if !familyMode || match.key.Signal != telemetrytypes.SignalTraces {
|
||||
fields = append(fields, telemetrytypes.SingleLogicalField(requestedName, match.key))
|
||||
continue
|
||||
}
|
||||
|
||||
identity := match.key.Signal.StringValue() + ";" + match.key.FieldContext.StringValue() + ";" + match.key.FieldDataType.StringValue()
|
||||
group, ok := groups[identity]
|
||||
if !ok {
|
||||
group = &telemetrytypes.LogicalField{
|
||||
Name: requestedName,
|
||||
Signal: match.key.Signal,
|
||||
FieldContext: match.key.FieldContext,
|
||||
FieldDataType: match.key.FieldDataType,
|
||||
contextPrefixedFieldName := fmt.Sprintf("%s.%s", field.FieldContext.StringValue(), field.Name)
|
||||
for _, item := range fieldKeys[contextPrefixedFieldName] {
|
||||
// Context already matched via the lookup key; only data type needs checking.
|
||||
if field.FieldDataType == telemetrytypes.FieldDataTypeUnspecified || item.FieldDataType == field.FieldDataType {
|
||||
fieldKeysForName = append(fieldKeysForName, item)
|
||||
}
|
||||
groups[identity] = group
|
||||
fields = append(fields, group)
|
||||
}
|
||||
if groupHasMemberNamed(group, match.key.Name) {
|
||||
continue
|
||||
}
|
||||
ranks[match.key] = match.rank
|
||||
group.Members = append(group.Members, match.key)
|
||||
}
|
||||
|
||||
for _, logical := range fields {
|
||||
slices.SortStableFunc(logical.Members, func(a, b *telemetrytypes.TelemetryFieldKey) int {
|
||||
return ranks[a] - ranks[b]
|
||||
})
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func groupHasMemberNamed(group *telemetrytypes.LogicalField, name string) bool {
|
||||
for _, member := range group.Members {
|
||||
if member.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return fieldKeysForName
|
||||
}
|
||||
|
||||
@@ -588,11 +588,9 @@ func TestVisitKey(t *testing.T) {
|
||||
|
||||
// VisitKey only parses; the condition builder matches, resolves ambiguity
|
||||
// and decides not-found handling. Replay that here against the generic
|
||||
// builder behavior (error unless the key is ignored). The test maps carry
|
||||
// no signal, so every logical field is single-member and flattens losslessly.
|
||||
matching := MatchingLogicalFields(context.Background(), valuer.UUID{}, nil, key, tt.fieldKeys)
|
||||
resolved, warning := ResolveLogicalFields(key, matching)
|
||||
keys := SingleKeys(resolved)
|
||||
// builder behavior (error unless the key is ignored).
|
||||
matching := MatchingFieldKeys(key, tt.fieldKeys)
|
||||
keys, warning := ResolveKeys(key, matching)
|
||||
|
||||
var gotErrors []string
|
||||
var gotMainErrURL, gotMainWrnURL string
|
||||
@@ -768,8 +766,7 @@ func (b *resourceConditionBuilder) ConditionFor(
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
resolved, warning := ResolveLogicalFields(key, MatchingLogicalFields(context.Background(), valuer.UUID{}, nil, key, fieldKeys))
|
||||
keys := SingleKeys(resolved)
|
||||
keys, warning := ResolveKeys(key, MatchingFieldKeys(key, fieldKeys))
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
@@ -811,8 +808,7 @@ func (b *conditionBuilder) ConditionFor(
|
||||
return []string{fmt.Sprintf("%s_cond", key.Name)}, nil, nil
|
||||
}
|
||||
|
||||
resolved, warning := ResolveLogicalFields(key, MatchingLogicalFields(context.Background(), valuer.UUID{}, nil, key, fieldKeys))
|
||||
keys := SingleKeys(resolved)
|
||||
keys, warning := ResolveKeys(key, MatchingFieldKeys(key, fieldKeys))
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
|
||||
@@ -241,8 +241,6 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewBackfillSavedViewRequestTypeFactory(sqlstore),
|
||||
sqlmigration.NewRestructureAuthDomainConfigFactory(sqlstore),
|
||||
sqlmigration.NewFixSavedViewSelectFieldsFactory(sqlstore),
|
||||
sqlmigration.NewDeleteOrphanUserRolesFactory(),
|
||||
sqlmigration.NewMigrateLambdaDashboardsFactory(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type deleteOrphanUserRoles struct{}
|
||||
|
||||
func NewDeleteOrphanUserRolesFactory() factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("delete_orphan_user_roles"),
|
||||
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &deleteOrphanUserRoles{}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (migration *deleteOrphanUserRoles) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *deleteOrphanUserRoles) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
var deletedUserIDs []string
|
||||
err = tx.NewSelect().
|
||||
Model(new(types.User)).
|
||||
Column("id").
|
||||
Where("status = ?", types.UserStatusDeleted).
|
||||
Scan(ctx, &deletedUserIDs)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(deletedUserIDs) == 0 {
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
_, err = tx.NewDelete().
|
||||
Model(new(authtypes.UserRole)).
|
||||
Where("user_id IN (?)", bun.In(deletedUserIDs)).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *deleteOrphanUserRoles) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
//go:embed 116_migrate_lambda_dashboards
|
||||
var lambdaDashboardFiles embed.FS
|
||||
|
||||
// These values mirror the cloud integration and dashboard packages but are duplicated
|
||||
// here so this migration keeps targeting and writing the same rows even if those
|
||||
// constants are later renamed or changed.
|
||||
const (
|
||||
lambdaDashboardFile = "116_migrate_lambda_dashboards/aws/lambda/overview.json"
|
||||
|
||||
lambdaDashboardSlug = "aws-lambda-overview"
|
||||
cloudIntegrationDashboardProvider = "cloud_integration"
|
||||
integrationDashboardSource = "integration"
|
||||
dashboardSchemaVersion = "v6"
|
||||
)
|
||||
|
||||
type migrateLambdaDashboards struct{}
|
||||
|
||||
type lambdaDashboardRow struct {
|
||||
bun.BaseModel `bun:"table:dashboard,alias:dashboard"`
|
||||
|
||||
ID string `bun:"id"`
|
||||
Data string `bun:"data"`
|
||||
}
|
||||
|
||||
// lambdaDashboardDefinition is the part of the embedded dashboard this migration reads:
|
||||
// its spec, which is what the cloud integration stores under data.spec.
|
||||
type lambdaDashboardDefinition struct {
|
||||
Spec map[string]any `json:"spec"`
|
||||
}
|
||||
|
||||
func NewMigrateLambdaDashboardsFactory() factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("migrate_lambda_dashboards"),
|
||||
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &migrateLambdaDashboards{}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (m *migrateLambdaDashboards) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(m.Up, m.Down)
|
||||
}
|
||||
|
||||
// Up rewrites the spec of every provisioned AWS Lambda overview dashboard to the
|
||||
// embedded revision that added the FunctionName variable. Cloud integration dashboards
|
||||
// are provisioned once and never updated afterwards, so existing installs only pick up
|
||||
// this change through a migration. Only the spec is replaced; the row keeps its id, name,
|
||||
// tags and metadata, so the dashboard is updated in place rather than recreated.
|
||||
func (m *migrateLambdaDashboards) Up(ctx context.Context, db *bun.DB) error {
|
||||
spec, err := m.loadSpec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
var rows []*lambdaDashboardRow
|
||||
if err := tx.NewSelect().
|
||||
Model(&rows).
|
||||
Join("JOIN integration_dashboard AS id ON id.dashboard_id = dashboard.id").
|
||||
Where("id.provider = ?", cloudIntegrationDashboardProvider).
|
||||
Where("id.slug = ?", lambdaDashboardSlug).
|
||||
Where("dashboard.source = ?", integrationDashboardSource).
|
||||
Scan(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
data := map[string]any{}
|
||||
if err := json.Unmarshal([]byte(row.Data), &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The embedded spec is v6-shaped, so only rewrite a row already carrying a v6 spec;
|
||||
// anything else is left alone rather than turned into a broken mix of versions.
|
||||
if !m.hasV6Spec(data) {
|
||||
continue
|
||||
}
|
||||
data["spec"] = spec
|
||||
|
||||
encoded, err := m.marshalUnescaped(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Skip rows already carrying this spec so a re-run does not needlessly rewrite them.
|
||||
if string(encoded) == row.Data {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := tx.NewUpdate().
|
||||
Model((*lambdaDashboardRow)(nil)).
|
||||
Set("data = ?", string(encoded)).
|
||||
Set("updated_at = ?", time.Now()).
|
||||
Where("id = ?", row.ID).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (m *migrateLambdaDashboards) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasV6Spec reports whether the stored data is a v6 dashboard with a spec object, which
|
||||
// is the shape whose spec this migration replaces.
|
||||
func (m *migrateLambdaDashboards) hasV6Spec(data map[string]any) bool {
|
||||
metadata, _ := data["metadata"].(map[string]any)
|
||||
version, _ := metadata["schemaVersion"].(string)
|
||||
if version != dashboardSchemaVersion {
|
||||
return false
|
||||
}
|
||||
_, ok := data["spec"].(map[string]any)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (m *migrateLambdaDashboards) marshalUnescaped(v any) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if err := encoder.Encode(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bytes.TrimRight(buf.Bytes(), "\n"), nil
|
||||
}
|
||||
|
||||
func (m *migrateLambdaDashboards) loadSpec() (map[string]any, error) {
|
||||
raw, err := lambdaDashboardFiles.ReadFile(lambdaDashboardFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dashboard lambdaDashboardDefinition
|
||||
if err := json.Unmarshal(raw, &dashboard); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dashboard.Spec, nil
|
||||
}
|
||||
@@ -1,856 +0,0 @@
|
||||
{
|
||||
"schemaVersion": "v6",
|
||||
"image": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBmaWxsPSJub25lIj48cGF0aCBmaWxsPSIjRkE3RTE0IiBkPSJNNy45ODMgOC4zN2MtLjA1My4wNzMtLjA5OC4xMzMtLjE0MS4xOTRMNS43NzUgMTEuNWMtLjY0LjkxLTEuMjgyIDEuODItMS45MjQgMi43M2EuMTI4LjEyOCAwIDAxLS4wOTIuMDUxYy0uOTA2LS4wMDctMS44MTMtLjAxNy0yLjcxOS0uMDI4LS4wMSAwLS4wMi0uMDAzLS4wNC0uMDA2YS40NTUuNDU1IDAgMDEuMDI1LS4wNTMgMTM5NzcuNDk2IDEzOTc3LjQ5NiAwIDAxNS40NDYtOC4xNDZjLjA5Mi0uMTM4LjE4OC0uMjczLjI3NS0uNDEzYS4xNjUuMTY1IDAgMDAuMDE4LS4xMjRjLS4xNjctLjUxNS0uMzM4LTEuMDMtLjUwOC0xLjU0My0uMDczLS4yMi0uMTUtLjQ0LS4yMTgtLjY2LS4wMjItLjA3Mi0uMDU5LS4wOTQtLjEzNC0uMDkzLS41Ny4wMDItMS4xMzYuMDAxLTEuNzA0LjAwMS0uMTA4IDAtLjEwOCAwLS4xMDgtLjEwMyAwLS42NzQgMC0xLjM0Ny0uMDAyLTIuMDIxIDAtLjA3NS4wMjYtLjA5Mi4wOTktLjA5MiAxLjE0My4wMDIgMi4yODYuMDAyIDMuNDMgMGEuMTEzLjExMyAwIDAxLjA3Ni4wMTcuMTA3LjEwNyAwIDAxLjA0NS4wNjEgMTgyNjYuMTg0IDE4MjY2LjE4NCAwIDAwMy45MiA5LjUxYy4yMTguNTMuNDM4IDEuMDU5LjY1NCAxLjU5LjAyNi4wNjQuMDUzLjA3Ni4xMi4wNTYuNi0uMTc4IDEuMi0uMzUyIDEuOC0uNTMxLjA3NS0uMDIzLjEwMi0uMDA4LjEyNi4wNjQuMjA0LjYyLjQxMiAxLjIzOS42MiAxLjg1OGwuMDIuMDczYy0uMDQzLjAxNS0uMDgzLjAzMi0uMTI0LjA0M2wtNC4wODUgMS4yNWMtLjA2NS4wMi0uMDg1IDAtLjEwNi0uMDU0bC0xLjI1LTMuMDQ4LTEuMjI2LTIuOTg0LS4xODMtLjQ0OWMtLjAxLS4wMjYtLjAyMy0uMDQ4LS4wNDMtLjA4N3oiLz48L3N2Zz4=",
|
||||
"name": "",
|
||||
"generateName": true,
|
||||
"tags": [],
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "AWS Lambda Overview",
|
||||
"description": "Overview of AWS Lambda functions"
|
||||
},
|
||||
"variables": [
|
||||
{
|
||||
"kind": "ListVariable",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "Account",
|
||||
"description": "AWS Account"
|
||||
},
|
||||
"allowAllValue": false,
|
||||
"allowMultiple": false,
|
||||
"customAllValue": "",
|
||||
"capturingRegexp": "",
|
||||
"sort": "none",
|
||||
"plugin": {
|
||||
"kind": "signoz/QueryVariable",
|
||||
"spec": {
|
||||
"queryValue": "SELECT JSONExtractString(labels, 'cloud.account.id') as `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_Lambda_Invocations_sum'\nGROUP BY `cloud.account.id`\n\n"
|
||||
}
|
||||
},
|
||||
"name": "Account"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "ListVariable",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "Region",
|
||||
"description": "AWS Region"
|
||||
},
|
||||
"allowAllValue": false,
|
||||
"allowMultiple": false,
|
||||
"customAllValue": "",
|
||||
"capturingRegexp": "",
|
||||
"sort": "none",
|
||||
"plugin": {
|
||||
"kind": "signoz/QueryVariable",
|
||||
"spec": {
|
||||
"queryValue": "SELECT JSONExtractString(labels, 'cloud.region') as `cloud.region`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_Lambda_Invocations_sum'\n and JSONExtractString(labels, 'cloud.account.id') IN {{.Account}}\nGROUP BY `cloud.region`\n"
|
||||
}
|
||||
},
|
||||
"name": "Region"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "ListVariable",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "FunctionName",
|
||||
"description": "Name of the Lambda function"
|
||||
},
|
||||
"allowAllValue": true,
|
||||
"allowMultiple": true,
|
||||
"customAllValue": "",
|
||||
"capturingRegexp": "",
|
||||
"sort": "none",
|
||||
"plugin": {
|
||||
"kind": "signoz/DynamicVariable",
|
||||
"spec": {
|
||||
"name": "FunctionName",
|
||||
"signal": "metrics"
|
||||
}
|
||||
},
|
||||
"name": "FunctionName"
|
||||
}
|
||||
}
|
||||
],
|
||||
"panels": {
|
||||
"2516c785-b025-49b3-aeb4-a4735ccb2709": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "Errors",
|
||||
"description": "The number of invocations that result in a function error. Function errors include exceptions that your code throws and exceptions that the Lambda runtime throws. The runtime returns errors for issues such as timeouts and configuration errors. To calculate the error rate, divide the value of Errors by the value of Invocations. Note that the timestamp on an error metric reflects when the function was invoked, not when the error occurred.\n\nSee more at https://docs.aws.amazon.com/lambda/latest/dg/monitoring-metrics-types.html"
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"timePreference": "global_time",
|
||||
"fillSpans": false
|
||||
},
|
||||
"formatting": {
|
||||
"unit": "none",
|
||||
"decimalPrecision": "2"
|
||||
},
|
||||
"chartAppearance": {
|
||||
"lineInterpolation": "spline",
|
||||
"showPoints": false,
|
||||
"lineStyle": "solid",
|
||||
"fillMode": "none",
|
||||
"spanGaps": {
|
||||
"fillOnlyBelow": false,
|
||||
"fillLessThan": ""
|
||||
}
|
||||
},
|
||||
"axes": {
|
||||
"softMin": 0,
|
||||
"softMax": 0,
|
||||
"isLogScale": false
|
||||
},
|
||||
"legend": {
|
||||
"position": "bottom",
|
||||
"mode": "list",
|
||||
"customColors": null
|
||||
},
|
||||
"thresholds": null
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "time_series",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"plugin": {
|
||||
"kind": "signoz/BuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"stepInterval": 60,
|
||||
"signal": "metrics",
|
||||
"source": "",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "aws_Lambda_Errors_sum",
|
||||
"temporality": "",
|
||||
"timeAggregation": "sum",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
],
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS) AND FunctionName IN $FunctionName"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
"name": "cloud.account.id",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
},
|
||||
{
|
||||
"name": "cloud.region",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
},
|
||||
{
|
||||
"name": "FunctionName",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
}
|
||||
],
|
||||
"order": [],
|
||||
"having": {
|
||||
"expression": ""
|
||||
},
|
||||
"functions": [],
|
||||
"legend": "{{FunctionName}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
},
|
||||
"4119a1e5-32a8-4859-96e9-a5451114782b": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "Async events dropped",
|
||||
"description": "The number of events that are dropped without successfully executing the function. If you configure a dead-letter queue (DLQ) or OnFailure destination, then events are sent there before they're dropped. Events are dropped for various reasons. For example, events can exceed the maximum event age or exhaust the maximum retry attempts, or reserved concurrency might be set to 0. To troubleshoot why events are dropped, look at the Errors metric to identify function errors and the Throttles metric to identify concurrency issues.\n\nSee more at https://docs.aws.amazon.com/lambda/latest/dg/monitoring-metrics-types.html"
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"timePreference": "global_time",
|
||||
"fillSpans": false
|
||||
},
|
||||
"formatting": {
|
||||
"unit": "none",
|
||||
"decimalPrecision": "2"
|
||||
},
|
||||
"chartAppearance": {
|
||||
"lineInterpolation": "spline",
|
||||
"showPoints": false,
|
||||
"lineStyle": "solid",
|
||||
"fillMode": "none",
|
||||
"spanGaps": {
|
||||
"fillOnlyBelow": false,
|
||||
"fillLessThan": ""
|
||||
}
|
||||
},
|
||||
"axes": {
|
||||
"softMin": 0,
|
||||
"softMax": 0,
|
||||
"isLogScale": false
|
||||
},
|
||||
"legend": {
|
||||
"position": "bottom",
|
||||
"mode": "list",
|
||||
"customColors": null
|
||||
},
|
||||
"thresholds": null
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "time_series",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"plugin": {
|
||||
"kind": "signoz/BuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"stepInterval": 60,
|
||||
"signal": "metrics",
|
||||
"source": "",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "aws_Lambda_AsyncEventsDropped_sum",
|
||||
"temporality": "",
|
||||
"timeAggregation": "sum",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
],
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS) AND FunctionName IN $FunctionName"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
"name": "cloud.account.id",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
},
|
||||
{
|
||||
"name": "cloud.region",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
},
|
||||
{
|
||||
"name": "FunctionName",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
}
|
||||
],
|
||||
"order": [],
|
||||
"having": {
|
||||
"expression": ""
|
||||
},
|
||||
"functions": [],
|
||||
"legend": "{{FunctionName}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
},
|
||||
"6354ea62-e82b-4323-a33d-eef92519e843": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "Throttles",
|
||||
"description": "The number of invocation requests that are throttled. When all function instances are processing requests and no concurrency is available to scale up, Lambda rejects additional requests with a TooManyRequestsException error. Throttled requests and other invocation errors don't count as either Invocations or Errors.\n\nSee more at https://docs.aws.amazon.com/lambda/latest/dg/monitoring-metrics-types.html"
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"timePreference": "global_time",
|
||||
"fillSpans": false
|
||||
},
|
||||
"formatting": {
|
||||
"unit": "none",
|
||||
"decimalPrecision": "2"
|
||||
},
|
||||
"chartAppearance": {
|
||||
"lineInterpolation": "spline",
|
||||
"showPoints": false,
|
||||
"lineStyle": "solid",
|
||||
"fillMode": "none",
|
||||
"spanGaps": {
|
||||
"fillOnlyBelow": false,
|
||||
"fillLessThan": ""
|
||||
}
|
||||
},
|
||||
"axes": {
|
||||
"softMin": 0,
|
||||
"softMax": 0,
|
||||
"isLogScale": false
|
||||
},
|
||||
"legend": {
|
||||
"position": "bottom",
|
||||
"mode": "list",
|
||||
"customColors": null
|
||||
},
|
||||
"thresholds": null
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "time_series",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"plugin": {
|
||||
"kind": "signoz/BuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"stepInterval": 60,
|
||||
"signal": "metrics",
|
||||
"source": "",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "aws_Lambda_Throttles_sum",
|
||||
"temporality": "",
|
||||
"timeAggregation": "sum",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
],
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS) AND FunctionName IN $FunctionName"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
"name": "cloud.account.id",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
},
|
||||
{
|
||||
"name": "cloud.region",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
},
|
||||
{
|
||||
"name": "FunctionName",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
}
|
||||
],
|
||||
"order": [],
|
||||
"having": {
|
||||
"expression": ""
|
||||
},
|
||||
"functions": [],
|
||||
"legend": "{{FunctionName}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
},
|
||||
"853d3a92-b396-4064-8762-18d7487989e0": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "Async events received",
|
||||
"description": "The number of events that Lambda successfully queues for processing. This metric provides insight into the number of events that a Lambda function receives. Monitor this metric and set alarms for thresholds to check for issues. For example, to detect an undesirable number of events sent to Lambda, and to quickly diagnose issues resulting from incorrect trigger or function configurations. Mismatches between AsyncEventsReceived and Invocations can indicate a disparity in processing, events being dropped, or a potential queue backlog.\n\nSee more at https://docs.aws.amazon.com/lambda/latest/dg/monitoring-metrics-types.html"
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"timePreference": "global_time",
|
||||
"fillSpans": false
|
||||
},
|
||||
"formatting": {
|
||||
"unit": "none",
|
||||
"decimalPrecision": "2"
|
||||
},
|
||||
"chartAppearance": {
|
||||
"lineInterpolation": "spline",
|
||||
"showPoints": false,
|
||||
"lineStyle": "solid",
|
||||
"fillMode": "none",
|
||||
"spanGaps": {
|
||||
"fillOnlyBelow": false,
|
||||
"fillLessThan": ""
|
||||
}
|
||||
},
|
||||
"axes": {
|
||||
"softMin": 0,
|
||||
"softMax": 0,
|
||||
"isLogScale": false
|
||||
},
|
||||
"legend": {
|
||||
"position": "bottom",
|
||||
"mode": "list",
|
||||
"customColors": null
|
||||
},
|
||||
"thresholds": null
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "time_series",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"plugin": {
|
||||
"kind": "signoz/BuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"stepInterval": 60,
|
||||
"signal": "metrics",
|
||||
"source": "",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "aws_Lambda_AsyncEventsReceived_sum",
|
||||
"temporality": "",
|
||||
"timeAggregation": "sum",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
],
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS) AND FunctionName IN $FunctionName"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
"name": "cloud.account.id",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
},
|
||||
{
|
||||
"name": "cloud.region",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
},
|
||||
{
|
||||
"name": "FunctionName",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
}
|
||||
],
|
||||
"order": [],
|
||||
"having": {
|
||||
"expression": ""
|
||||
},
|
||||
"functions": [],
|
||||
"legend": "{{FunctionName}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
},
|
||||
"877bb5c8-331c-492f-b666-2054c2ae39bd": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "Invocations",
|
||||
"description": "The number of times that your function code is invoked, including successful invocations and invocations that result in a function error. Invocations aren't recorded if the invocation request is throttled or otherwise results in an invocation error. The value of Invocations equals the number of requests billed.\n\nSee more at https://docs.aws.amazon.com/lambda/latest/dg/monitoring-metrics-types.html"
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"timePreference": "global_time",
|
||||
"fillSpans": false
|
||||
},
|
||||
"formatting": {
|
||||
"unit": "none",
|
||||
"decimalPrecision": "2"
|
||||
},
|
||||
"chartAppearance": {
|
||||
"lineInterpolation": "spline",
|
||||
"showPoints": false,
|
||||
"lineStyle": "solid",
|
||||
"fillMode": "none",
|
||||
"spanGaps": {
|
||||
"fillOnlyBelow": false,
|
||||
"fillLessThan": ""
|
||||
}
|
||||
},
|
||||
"axes": {
|
||||
"softMin": 0,
|
||||
"softMax": 0,
|
||||
"isLogScale": false
|
||||
},
|
||||
"legend": {
|
||||
"position": "bottom",
|
||||
"mode": "list",
|
||||
"customColors": null
|
||||
},
|
||||
"thresholds": null
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "time_series",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"plugin": {
|
||||
"kind": "signoz/BuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"stepInterval": 60,
|
||||
"signal": "metrics",
|
||||
"source": "",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "aws_Lambda_Invocations_sum",
|
||||
"temporality": "",
|
||||
"timeAggregation": "sum",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
],
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS) AND FunctionName IN $FunctionName"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
"name": "cloud.account.id",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
},
|
||||
{
|
||||
"name": "cloud.region",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
},
|
||||
{
|
||||
"name": "FunctionName",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
}
|
||||
],
|
||||
"order": [],
|
||||
"having": {
|
||||
"expression": ""
|
||||
},
|
||||
"functions": [],
|
||||
"legend": "{{FunctionName}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
},
|
||||
"ae6d7c81-d921-4d4c-95ec-6b42d900ea45": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "Max Async Event Age",
|
||||
"description": "The time between when Lambda successfully queues the event and when the function is invoked. The value of this metric increases when events are being retried due to invocation failures or throttling. Monitor this metric and set alarms for thresholds on different statistics for when a queue buildup occurs. To troubleshoot an increase in this metric, look at the Errors metric to identify function errors and the Throttles metric to identify concurrency issues.\n\nSee more at https://docs.aws.amazon.com/lambda/latest/dg/monitoring-metrics-types.html"
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"timePreference": "global_time",
|
||||
"fillSpans": false
|
||||
},
|
||||
"formatting": {
|
||||
"unit": "ms",
|
||||
"decimalPrecision": "2"
|
||||
},
|
||||
"chartAppearance": {
|
||||
"lineInterpolation": "spline",
|
||||
"showPoints": false,
|
||||
"lineStyle": "solid",
|
||||
"fillMode": "none",
|
||||
"spanGaps": {
|
||||
"fillOnlyBelow": false,
|
||||
"fillLessThan": ""
|
||||
}
|
||||
},
|
||||
"axes": {
|
||||
"softMin": 0,
|
||||
"softMax": 0,
|
||||
"isLogScale": false
|
||||
},
|
||||
"legend": {
|
||||
"position": "bottom",
|
||||
"mode": "list",
|
||||
"customColors": null
|
||||
},
|
||||
"thresholds": null
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "time_series",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"plugin": {
|
||||
"kind": "signoz/BuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"stepInterval": 60,
|
||||
"signal": "metrics",
|
||||
"source": "",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "aws_Lambda_AsyncEventAge_max",
|
||||
"temporality": "",
|
||||
"timeAggregation": "max",
|
||||
"spaceAggregation": "max",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
],
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS) AND FunctionName IN $FunctionName"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
"name": "cloud.account.id",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
},
|
||||
{
|
||||
"name": "cloud.region",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
},
|
||||
{
|
||||
"name": "FunctionName",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
}
|
||||
],
|
||||
"order": [],
|
||||
"having": {
|
||||
"expression": ""
|
||||
},
|
||||
"functions": [],
|
||||
"legend": "{{FunctionName}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
},
|
||||
"b038520d-0756-4e46-a915-12a2f19a0254": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "Max Duration",
|
||||
"description": "The amount of time that your function code spends processing an event. The billed duration for an invocation is the value of Duration rounded up to the nearest millisecond. Duration does not include cold start time.\n\nSee more at https://docs.aws.amazon.com/lambda/latest/dg/monitoring-metrics-types.html"
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"timePreference": "global_time",
|
||||
"fillSpans": false
|
||||
},
|
||||
"formatting": {
|
||||
"unit": "ms",
|
||||
"decimalPrecision": "2"
|
||||
},
|
||||
"chartAppearance": {
|
||||
"lineInterpolation": "spline",
|
||||
"showPoints": false,
|
||||
"lineStyle": "solid",
|
||||
"fillMode": "none",
|
||||
"spanGaps": {
|
||||
"fillOnlyBelow": false,
|
||||
"fillLessThan": ""
|
||||
}
|
||||
},
|
||||
"axes": {
|
||||
"softMin": 0,
|
||||
"softMax": 0,
|
||||
"isLogScale": false
|
||||
},
|
||||
"legend": {
|
||||
"position": "bottom",
|
||||
"mode": "list",
|
||||
"customColors": null
|
||||
},
|
||||
"thresholds": null
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "time_series",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"plugin": {
|
||||
"kind": "signoz/BuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"stepInterval": 60,
|
||||
"signal": "metrics",
|
||||
"source": "",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "aws_Lambda_Duration_max",
|
||||
"temporality": "",
|
||||
"timeAggregation": "max",
|
||||
"spaceAggregation": "max",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
],
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "(cloud.account.id = $Account AND cloud.region = $Region AND FunctionName EXISTS AND Resource NOT EXISTS) AND FunctionName IN $FunctionName"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
"name": "cloud.account.id",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
},
|
||||
{
|
||||
"name": "cloud.region",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
},
|
||||
{
|
||||
"name": "FunctionName",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
}
|
||||
],
|
||||
"order": [],
|
||||
"having": {
|
||||
"expression": ""
|
||||
},
|
||||
"functions": [],
|
||||
"legend": "{{FunctionName}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"layouts": [
|
||||
{
|
||||
"kind": "Grid",
|
||||
"spec": {
|
||||
"items": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/877bb5c8-331c-492f-b666-2054c2ae39bd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/b038520d-0756-4e46-a915-12a2f19a0254"
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 0,
|
||||
"y": 6,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/2516c785-b025-49b3-aeb4-a4735ccb2709"
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 6,
|
||||
"y": 6,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/6354ea62-e82b-4323-a33d-eef92519e843"
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 0,
|
||||
"y": 12,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/853d3a92-b396-4064-8762-18d7487989e0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 6,
|
||||
"y": 12,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/ae6d7c81-d921-4d4c-95ec-6b42d900ea45"
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 0,
|
||||
"y": 18,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/4119a1e5-32a8-4859-96e9-a5451114782b"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"duration": "",
|
||||
"refreshInterval": "",
|
||||
"links": []
|
||||
}
|
||||
}
|
||||
@@ -322,38 +322,6 @@ func TestStatementBuilder(t *testing.T) {
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "test_bool_label_filter",
|
||||
requestType: qbtypes.RequestTypeTimeSeries,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "signoz_calls_total",
|
||||
Type: metrictypes.SumType,
|
||||
Temporality: metrictypes.Cumulative,
|
||||
TimeAggregation: metrictypes.TimeAggregationRate,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "success = true",
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "service.name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __temporal_aggregation_cte AS (SELECT ts, `__GROUP_BY_KEY_0_service.name`, multiIf(row_number() OVER rate_window = 1, nan, (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) < 0, per_series_value / (ts - lagInFrame(ts, 1) OVER rate_window), (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) / (ts - lagInFrame(ts, 1) OVER rate_window)) AS per_series_value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(30)) AS ts, `__GROUP_BY_KEY_0_service.name`, max(value) AS per_series_value FROM signoz_metrics.distributed_samples_v4 AS points INNER JOIN (SELECT fingerprint, JSONExtractString(labels, 'service.name') AS `__GROUP_BY_KEY_0_service.name` FROM signoz_metrics.time_series_v4_6hrs WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND accurateCastOrNull(JSONExtractString(labels, 'success'), 'Bool') = ? GROUP BY fingerprint, `__GROUP_BY_KEY_0_service.name`) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts, `__GROUP_BY_KEY_0_service.name` ORDER BY fingerprint, ts) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)), __spatial_aggregation_cte AS (SELECT ts, `__GROUP_BY_KEY_0_service.name`, sum(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts, `__GROUP_BY_KEY_0_service.name`) SELECT * FROM __spatial_aggregation_cte ORDER BY `__GROUP_BY_KEY_0_service.name`, ts",
|
||||
Args: []any{"signoz_calls_total", uint64(1747936800000), uint64(1747983420000), "cumulative", true, "signoz_calls_total", uint64(1747947360000), uint64(1747983420000), 0},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
fm := metricstelemetryschema.NewFieldMapper()
|
||||
|
||||
@@ -31,14 +31,6 @@
|
||||
"signal": "metrics"
|
||||
}
|
||||
],
|
||||
"success": [
|
||||
{
|
||||
"name": "success",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "bool",
|
||||
"signal": "metrics"
|
||||
}
|
||||
],
|
||||
"materialized.key.name": [
|
||||
{
|
||||
"name": "materialized.key.name",
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
@@ -15,15 +14,12 @@ import (
|
||||
|
||||
type defaultConditionBuilder struct {
|
||||
fm qbtypes.FieldMapper
|
||||
// fl evaluates the resolve_semconv_families flag during resolution.
|
||||
// A nil flagger keeps resolution literal.
|
||||
fl flagger.Flagger
|
||||
}
|
||||
|
||||
var _ qbtypes.ConditionBuilder = (*defaultConditionBuilder)(nil)
|
||||
|
||||
func NewConditionBuilder(fm qbtypes.FieldMapper, fl flagger.Flagger) *defaultConditionBuilder {
|
||||
return &defaultConditionBuilder{fm: fm, fl: fl}
|
||||
func NewConditionBuilder(fm qbtypes.FieldMapper) *defaultConditionBuilder {
|
||||
return &defaultConditionBuilder{fm: fm}
|
||||
}
|
||||
|
||||
func valueForIndexFilter(op qbtypes.FilterOperator, key *telemetrytypes.TelemetryFieldKey, value any) any {
|
||||
@@ -48,74 +44,10 @@ func keyIndexFilter(key *telemetrytypes.TelemetryFieldKey) any {
|
||||
return fmt.Sprintf(`%%%s%%`, key.Name)
|
||||
}
|
||||
|
||||
// The three helpers below take the members of one logical field. With a single
|
||||
// member they render exactly the pre-family shapes; a family widens key/value
|
||||
// index hints to any-member and presence to any-member (all-absent when negated).
|
||||
|
||||
func keyIndexCondition(sb *sqlbuilder.SelectBuilder, column string, members []*telemetrytypes.TelemetryFieldKey) string {
|
||||
conditions := make([]string, 0, len(members))
|
||||
for _, member := range members {
|
||||
conditions = append(conditions, sb.Like(column, keyIndexFilter(member)))
|
||||
}
|
||||
if len(conditions) == 1 {
|
||||
return conditions[0]
|
||||
}
|
||||
return sb.Or(conditions...)
|
||||
}
|
||||
|
||||
func valueIndexCondition(
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
column string,
|
||||
members []*telemetrytypes.TelemetryFieldKey,
|
||||
op qbtypes.FilterOperator,
|
||||
value any,
|
||||
caseInsensitive bool,
|
||||
) string {
|
||||
conditions := make([]string, 0, len(members))
|
||||
for _, member := range members {
|
||||
patterns := valueForIndexFilter(op, member, value)
|
||||
switch values := patterns.(type) {
|
||||
case []string:
|
||||
for _, pattern := range values {
|
||||
conditions = append(conditions, sb.Like(column, pattern))
|
||||
}
|
||||
default:
|
||||
if caseInsensitive {
|
||||
conditions = append(conditions, sb.ILike(column, values))
|
||||
} else {
|
||||
conditions = append(conditions, sb.Like(column, values))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(conditions) == 1 {
|
||||
return conditions[0]
|
||||
}
|
||||
return sb.Or(conditions...)
|
||||
}
|
||||
|
||||
func memberPresenceCondition(sb *sqlbuilder.SelectBuilder, column string, members []*telemetrytypes.TelemetryFieldKey, exists bool) string {
|
||||
conditions := make([]string, 0, len(members))
|
||||
for _, member := range members {
|
||||
field := fmt.Sprintf("simpleJSONHas(%s, '%s')", column, member.Name)
|
||||
if exists {
|
||||
conditions = append(conditions, sb.E(field, true))
|
||||
} else {
|
||||
conditions = append(conditions, sb.NE(field, true))
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
if len(conditions) == 1 {
|
||||
return conditions[0]
|
||||
}
|
||||
return sb.Or(conditions...)
|
||||
}
|
||||
return sb.And(conditions...)
|
||||
}
|
||||
|
||||
// SkipResourceFilter is not applicable here: the fingerprint table only stores resource attributes.
|
||||
func (b *defaultConditionBuilder) ConditionFor(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
_ valuer.UUID,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
@@ -125,7 +57,7 @@ func (b *defaultConditionBuilder) ConditionFor(
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
matches := querybuilder.MatchingLogicalFields(ctx, orgID, b.fl, key, fieldKeys)
|
||||
matches := querybuilder.MatchingFieldKeys(key, fieldKeys)
|
||||
|
||||
// has/hasAny/hasAll/hasToken are logs-body-only functions; they never apply to the
|
||||
// resource fingerprint table, so skip them (the main query still evaluates them).
|
||||
@@ -133,21 +65,21 @@ func (b *defaultConditionBuilder) ConditionFor(
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
logicalFields, warning := querybuilder.ResolveLogicalFields(key, matches)
|
||||
keys, warning := querybuilder.ResolveKeys(key, matches)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(logicalFields))
|
||||
for _, logical := range logicalFields {
|
||||
// the resource fingerprint table only stores resource attributes; fields from
|
||||
conds := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
// the resource fingerprint table only stores resource attributes; keys from
|
||||
// any other context contribute no condition and are omitted. An empty result
|
||||
// (including an unknown key) lets the caller skip this filter entirely.
|
||||
if logical.FieldContext != telemetrytypes.FieldContextResource {
|
||||
if k.FieldContext != telemetrytypes.FieldContextResource {
|
||||
continue
|
||||
}
|
||||
cond, err := b.conditionForLogicalField(ctx, orgID, startNs, endNs, logical, op, value, sb)
|
||||
cond, err := b.conditionForKey(ctx, startNs, endNs, k, op, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -156,12 +88,11 @@ func (b *defaultConditionBuilder) ConditionFor(
|
||||
return conds, warnings, nil
|
||||
}
|
||||
|
||||
func (b *defaultConditionBuilder) conditionForLogicalField(
|
||||
func (b *defaultConditionBuilder) conditionForKey(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
logical *telemetrytypes.LogicalField,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
op qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
@@ -171,9 +102,7 @@ func (b *defaultConditionBuilder) conditionForLogicalField(
|
||||
// as we store resource values as string
|
||||
formattedValue := querybuilder.FormatValueForContains(value)
|
||||
|
||||
// Every resource-context key maps to the labels column, so any member
|
||||
// resolves the column for the whole field.
|
||||
columns, err := b.fm.ColumnFor(ctx, orgID, startNs, endNs, logical.Single())
|
||||
columns, err := b.fm.ColumnFor(ctx, valuer.UUID{}, startNs, endNs, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -186,12 +115,10 @@ func (b *defaultConditionBuilder) conditionForLogicalField(
|
||||
// as we have not changed the resource column in the resource fingerprint table.
|
||||
column := columns[0]
|
||||
|
||||
members := logical.Members
|
||||
isFamily := logical.IsFamily()
|
||||
keyIdxFilter := keyIndexCondition(sb, column.Name, members)
|
||||
singleValueIndexFilter := valueForIndexFilter(op, members[0], value)
|
||||
keyIdxFilter := sb.Like(column.Name, keyIndexFilter(key))
|
||||
valueForIndexFilter := valueForIndexFilter(op, key, value)
|
||||
|
||||
fieldName, err := querybuilder.LogicalValueExpr(ctx, orgID, startNs, endNs, b.fm, logical)
|
||||
fieldName, err := b.fm.FieldFor(ctx, valuer.UUID{}, startNs, endNs, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -201,17 +128,12 @@ func (b *defaultConditionBuilder) conditionForLogicalField(
|
||||
return sb.And(
|
||||
sb.E(fieldName, formattedValue),
|
||||
keyIdxFilter,
|
||||
valueIndexCondition(sb, column.Name, members, op, value, false),
|
||||
sb.Like(column.Name, valueForIndexFilter),
|
||||
), nil
|
||||
case qbtypes.FilterOperatorNotEqual:
|
||||
if isFamily {
|
||||
// A negated value-index hint would drop rows where another member
|
||||
// holds the value; the fingerprint scan is small enough without it.
|
||||
return sb.NE(fieldName, formattedValue), nil
|
||||
}
|
||||
return sb.And(
|
||||
sb.NE(fieldName, formattedValue),
|
||||
sb.NotLike(column.Name, singleValueIndexFilter),
|
||||
sb.NotLike(column.Name, valueForIndexFilter),
|
||||
), nil
|
||||
case qbtypes.FilterOperatorGreaterThan:
|
||||
return sb.And(sb.GT(fieldName, formattedValue), keyIdxFilter), nil
|
||||
@@ -226,7 +148,7 @@ func (b *defaultConditionBuilder) conditionForLogicalField(
|
||||
return sb.And(
|
||||
sb.ILike(fieldName, formattedValue),
|
||||
keyIdxFilter,
|
||||
valueIndexCondition(sb, column.Name, members, op, value, true),
|
||||
sb.ILike(column.Name, valueForIndexFilter),
|
||||
), nil
|
||||
case qbtypes.FilterOperatorNotLike, qbtypes.FilterOperatorNotILike:
|
||||
// no index filter: as cannot apply `not contains x%y` as y can be somewhere else
|
||||
@@ -263,11 +185,13 @@ func (b *defaultConditionBuilder) conditionForLogicalField(
|
||||
inConditions = append(inConditions, sb.E(fieldName, querybuilder.FormatValueForContains(v)))
|
||||
}
|
||||
mainCondition := sb.Or(inConditions...)
|
||||
mainCondition = sb.And(
|
||||
mainCondition,
|
||||
keyIdxFilter,
|
||||
valueIndexCondition(sb, column.Name, members, op, value, false),
|
||||
)
|
||||
valConditions := make([]string, 0, len(values))
|
||||
if valuesForIndexFilter, ok := valueForIndexFilter.([]string); ok {
|
||||
for _, v := range valuesForIndexFilter {
|
||||
valConditions = append(valConditions, sb.Like(column.Name, v))
|
||||
}
|
||||
}
|
||||
mainCondition = sb.And(mainCondition, keyIdxFilter, sb.Or(valConditions...))
|
||||
|
||||
return mainCondition, nil
|
||||
case qbtypes.FilterOperatorNotIn:
|
||||
@@ -280,13 +204,8 @@ func (b *defaultConditionBuilder) conditionForLogicalField(
|
||||
notInConditions = append(notInConditions, sb.NE(fieldName, querybuilder.FormatValueForContains(v)))
|
||||
}
|
||||
mainCondition := sb.And(notInConditions...)
|
||||
if isFamily {
|
||||
// A negated value-index hint would drop rows where another member
|
||||
// holds the value; the fingerprint scan is small enough without it.
|
||||
return mainCondition, nil
|
||||
}
|
||||
valConditions := make([]string, 0, len(values))
|
||||
if valuesForIndexFilter, ok := singleValueIndexFilter.([]string); ok {
|
||||
if valuesForIndexFilter, ok := valueForIndexFilter.([]string); ok {
|
||||
for _, v := range valuesForIndexFilter {
|
||||
valConditions = append(valConditions, sb.NotLike(column.Name, v))
|
||||
}
|
||||
@@ -296,11 +215,13 @@ func (b *defaultConditionBuilder) conditionForLogicalField(
|
||||
|
||||
case qbtypes.FilterOperatorExists:
|
||||
return sb.And(
|
||||
memberPresenceCondition(sb, column.Name, members, true),
|
||||
sb.E(fmt.Sprintf("simpleJSONHas(%s, '%s')", column.Name, key.Name), true),
|
||||
keyIdxFilter,
|
||||
), nil
|
||||
case qbtypes.FilterOperatorNotExists:
|
||||
return memberPresenceCondition(sb, column.Name, members, false), nil
|
||||
return sb.And(
|
||||
sb.NE(fmt.Sprintf("simpleJSONHas(%s, '%s')", column.Name, key.Name), true),
|
||||
), nil
|
||||
|
||||
case qbtypes.FilterOperatorRegexp:
|
||||
return sb.And(
|
||||
@@ -316,7 +237,7 @@ func (b *defaultConditionBuilder) conditionForLogicalField(
|
||||
return sb.And(
|
||||
sb.ILike(fieldName, fmt.Sprintf(`%%%s%%`, formattedValue)),
|
||||
keyIdxFilter,
|
||||
valueIndexCondition(sb, column.Name, members, op, value, true),
|
||||
sb.ILike(column.Name, valueForIndexFilter),
|
||||
), nil
|
||||
case qbtypes.FilterOperatorNotContains:
|
||||
// no index filter: as cannot apply `not contains x%y` as y can be somewhere else
|
||||
|
||||
@@ -2,7 +2,6 @@ package resourcefilter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
|
||||
"testing"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
@@ -202,7 +201,7 @@ func TestConditionBuilder(t *testing.T) {
|
||||
}
|
||||
|
||||
fm := NewFieldMapper()
|
||||
conditionBuilder := NewConditionBuilder(fm, flaggertest.New(t))
|
||||
conditionBuilder := NewConditionBuilder(fm)
|
||||
|
||||
for _, tc := range testCases {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
package resourcefilter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// The tests below run against the deployment.environment(.name) family with
|
||||
// the resolve_semconv_families flag on, and with both spellings present in
|
||||
// the metadata map as trace resource attributes.
|
||||
|
||||
func TestFamilyEqualWidensIndexHintsToAnyMember(t *testing.T) {
|
||||
cb := NewConditionBuilder(NewFieldMapper(), flaggertest.WithBooleanFlags(t, map[string]bool{flagger.FeatureResolveSemconvFamilies.String(): true}))
|
||||
fieldKeys := map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"deployment.environment.name": {{
|
||||
Name: "deployment.environment.name",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
"deployment.environment": {{
|
||||
Name: "deployment.environment",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
conds, _, err := cb.ConditionFor(context.Background(), valuer.UUID{}, 0, 0,
|
||||
&telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"},
|
||||
fieldKeys, qbtypes.ConditionBuilderOptions{}, qbtypes.FilterOperatorEqual, "production", sb)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, conds, 1)
|
||||
sb.Where(conds...)
|
||||
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
require.Equal(t, "WHERE (COALESCE(NULLIF(simpleJSONExtractString(labels, 'deployment.environment.name'), ''), NULLIF(simpleJSONExtractString(labels, 'deployment.environment'), ''), '') = ? AND (labels LIKE ? OR labels LIKE ?) AND (labels LIKE ? OR labels LIKE ?))", sql)
|
||||
require.Equal(t, []any{"production", "%deployment.environment.name%", "%deployment.environment%", "%deployment.environment.name\":\"production%", "%deployment.environment\":\"production%"}, args)
|
||||
}
|
||||
|
||||
func TestFamilyNotEqualDropsNegatedValueHint(t *testing.T) {
|
||||
cb := NewConditionBuilder(NewFieldMapper(), flaggertest.WithBooleanFlags(t, map[string]bool{flagger.FeatureResolveSemconvFamilies.String(): true}))
|
||||
fieldKeys := map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"deployment.environment.name": {{
|
||||
Name: "deployment.environment.name",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
"deployment.environment": {{
|
||||
Name: "deployment.environment",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
conds, _, err := cb.ConditionFor(context.Background(), valuer.UUID{}, 0, 0,
|
||||
&telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"},
|
||||
fieldKeys, qbtypes.ConditionBuilderOptions{}, qbtypes.FilterOperatorNotEqual, "production", sb)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, conds, 1)
|
||||
sb.Where(conds...)
|
||||
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
require.Equal(t, "WHERE COALESCE(NULLIF(simpleJSONExtractString(labels, 'deployment.environment.name'), ''), NULLIF(simpleJSONExtractString(labels, 'deployment.environment'), ''), '') <> ?", sql)
|
||||
require.Equal(t, []any{"production"}, args)
|
||||
}
|
||||
|
||||
func TestFamilyExistsIsAnyMemberPresence(t *testing.T) {
|
||||
cb := NewConditionBuilder(NewFieldMapper(), flaggertest.WithBooleanFlags(t, map[string]bool{flagger.FeatureResolveSemconvFamilies.String(): true}))
|
||||
fieldKeys := map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"deployment.environment.name": {{
|
||||
Name: "deployment.environment.name",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
"deployment.environment": {{
|
||||
Name: "deployment.environment",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
conds, _, err := cb.ConditionFor(context.Background(), valuer.UUID{}, 0, 0,
|
||||
&telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"},
|
||||
fieldKeys, qbtypes.ConditionBuilderOptions{}, qbtypes.FilterOperatorExists, nil, sb)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, conds, 1)
|
||||
sb.Where(conds...)
|
||||
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
require.Equal(t, "WHERE ((simpleJSONHas(labels, 'deployment.environment.name') = ? OR simpleJSONHas(labels, 'deployment.environment') = ?) AND (labels LIKE ? OR labels LIKE ?))", sql)
|
||||
require.Equal(t, []any{true, true, "%deployment.environment.name%", "%deployment.environment%"}, args)
|
||||
|
||||
sb = sqlbuilder.NewSelectBuilder()
|
||||
conds, _, err = cb.ConditionFor(context.Background(), valuer.UUID{}, 0, 0,
|
||||
&telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"},
|
||||
fieldKeys, qbtypes.ConditionBuilderOptions{}, qbtypes.FilterOperatorNotExists, nil, sb)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, conds, 1)
|
||||
sb.Where(conds...)
|
||||
sql, args = sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
require.Equal(t, "WHERE (simpleJSONHas(labels, 'deployment.environment.name') <> ? AND simpleJSONHas(labels, 'deployment.environment') <> ?)", sql)
|
||||
require.Equal(t, []any{true, true}, args)
|
||||
}
|
||||
|
||||
// With only one member in metadata the SQL keeps the exact pre-family shape,
|
||||
// including the negated value hint on !=.
|
||||
func TestSingleMemberShapesUnchanged(t *testing.T) {
|
||||
cb := NewConditionBuilder(NewFieldMapper(), flaggertest.WithBooleanFlags(t, map[string]bool{flagger.FeatureResolveSemconvFamilies.String(): true}))
|
||||
fieldKeys := map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"deployment.environment.name": {{
|
||||
Name: "deployment.environment.name",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
conds, _, err := cb.ConditionFor(context.Background(), valuer.UUID{}, 0, 0,
|
||||
&telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"},
|
||||
fieldKeys, qbtypes.ConditionBuilderOptions{}, qbtypes.FilterOperatorNotEqual, "production", sb)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, conds, 1)
|
||||
sb.Where(conds...)
|
||||
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
require.Equal(t, "WHERE (simpleJSONExtractString(labels, 'deployment.environment.name') <> ? AND labels NOT LIKE ?)", sql)
|
||||
require.Equal(t, []any{"production", "%deployment.environment.name\":\"production%"}, args)
|
||||
}
|
||||
@@ -71,33 +71,6 @@ func (m *defaultFieldMapper) FieldFor(
|
||||
return columns[0].Name, nil
|
||||
}
|
||||
|
||||
// ExistsFor reports key presence in the fingerprint labels JSON. Only resource
|
||||
// context keys have a presence notion here; anything else is a real column and
|
||||
// always present.
|
||||
func (m *defaultFieldMapper) ExistsFor(
|
||||
ctx context.Context,
|
||||
_ valuer.UUID,
|
||||
tsStart, tsEnd uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
exists bool,
|
||||
) (string, error) {
|
||||
columns, err := m.getColumn(ctx, tsStart, tsEnd, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if key.FieldContext != telemetrytypes.FieldContextResource {
|
||||
if exists {
|
||||
return "true", nil
|
||||
}
|
||||
return "false", nil
|
||||
}
|
||||
pred := fmt.Sprintf("simpleJSONHas(%s, '%s')", columns[0].Name, key.Name)
|
||||
if exists {
|
||||
return pred, nil
|
||||
}
|
||||
return "NOT " + pred, nil
|
||||
}
|
||||
|
||||
func (m *defaultFieldMapper) ColumnExpressionFor(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
|
||||
@@ -47,7 +47,7 @@ func New[T any](
|
||||
) *resourceFilterStatementBuilder[T] {
|
||||
set := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/statementbuilder/resourcefilter")
|
||||
fm := NewFieldMapper()
|
||||
cb := NewConditionBuilder(fm, fl)
|
||||
cb := NewConditionBuilder(fm)
|
||||
return &resourceFilterStatementBuilder[T]{
|
||||
logger: set.Logger(),
|
||||
dbName: dbName,
|
||||
@@ -99,7 +99,7 @@ func (b *resourceFilterStatementBuilder[T]) Build(
|
||||
q.Select("fingerprint")
|
||||
q.From(fmt.Sprintf("%s.%s", b.dbName, b.tableName))
|
||||
|
||||
keySelectors := querybuilder.ExpandKeySelectorsForFamilies(ctx, orgID, b.flagger, b.getKeySelectors(query))
|
||||
keySelectors := b.getKeySelectors(query)
|
||||
keys, _, err := b.metadataStore.GetKeysMulti(ctx, orgID, keySelectors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -164,7 +164,6 @@ func (b *resourceFilterStatementBuilder[T]) addConditions(
|
||||
filterWhereClause, err := querybuilder.PrepareWhereClause(query.Filter.Expression, querybuilder.FilterExprVisitorOpts{
|
||||
Context: ctx,
|
||||
OrgID: orgID,
|
||||
Flagger: b.flagger,
|
||||
Logger: b.logger,
|
||||
FieldMapper: b.fieldMapper,
|
||||
ConditionBuilder: b.conditionBuilder,
|
||||
|
||||
@@ -38,7 +38,6 @@ type scopedTraceStatementBuilder struct {
|
||||
scope TraceScope
|
||||
traceStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
|
||||
resourceFilterStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
|
||||
fl flagger.Flagger
|
||||
}
|
||||
|
||||
var _ qbtypes.StatementBuilder[qbtypes.TraceAggregation] = (*scopedTraceStatementBuilder)(nil)
|
||||
@@ -60,8 +59,8 @@ func NewFactory(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fm := tracestelemetryschema.NewFieldMapper(fl)
|
||||
cb := tracestelemetryschema.NewConditionBuilder(fm, fl)
|
||||
fm := tracestelemetryschema.NewFieldMapper()
|
||||
cb := tracestelemetryschema.NewConditionBuilder(fm)
|
||||
return NewScopedTraceStatementBuilder(settings, metadataStore, fm, cb, scope, traceStmtBuilder, fl), nil
|
||||
},
|
||||
)
|
||||
@@ -99,7 +98,6 @@ func NewScopedTraceStatementBuilder(
|
||||
scope: scope,
|
||||
traceStmtBuilder: traceStmtBuilder,
|
||||
resourceFilterStmtBuilder: resourceFilterStmtBuilder,
|
||||
fl: fl,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,7 +267,7 @@ func (b *scopedTraceStatementBuilder) fetchKeys(ctx context.Context, orgID value
|
||||
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeExact,
|
||||
})
|
||||
}
|
||||
keys, _, err := b.metadataStore.GetKeysMulti(ctx, orgID, querybuilder.ExpandKeySelectorsForFamilies(ctx, orgID, b.fl, selectors))
|
||||
keys, _, err := b.metadataStore.GetKeysMulti(ctx, orgID, selectors)
|
||||
return keys, err
|
||||
}
|
||||
|
||||
@@ -446,14 +444,13 @@ func (b *scopedTraceStatementBuilder) resolveSpanPredicate(ctx context.Context,
|
||||
for i := range selectors {
|
||||
selectors[i].Signal = telemetrytypes.SignalTraces
|
||||
}
|
||||
keys, _, err := b.metadataStore.GetKeysMulti(ctx, orgID, querybuilder.ExpandKeySelectorsForFamilies(ctx, orgID, b.fl, selectors))
|
||||
keys, _, err := b.metadataStore.GetKeysMulti(ctx, orgID, selectors)
|
||||
if err != nil {
|
||||
return "", nil, "", err
|
||||
}
|
||||
prepared, err := querybuilder.PrepareWhereClause(expr, querybuilder.FilterExprVisitorOpts{
|
||||
Context: ctx,
|
||||
OrgID: orgID,
|
||||
Flagger: b.fl,
|
||||
Logger: b.logger,
|
||||
FieldMapper: b.fm,
|
||||
ConditionBuilder: b.cb,
|
||||
|
||||
@@ -31,7 +31,6 @@ type traceQueryStatementBuilder struct {
|
||||
cb qbtypes.ConditionBuilder
|
||||
resourceFilterResolver *resourcefilter.ResourceFingerprintResolver[qbtypes.TraceAggregation]
|
||||
aggExprRewriter qbtypes.AggExprRewriter
|
||||
fl flagger.Flagger
|
||||
skipResourceFingerprintEnabled bool
|
||||
}
|
||||
|
||||
@@ -48,8 +47,8 @@ func NewFactory(
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("traces"),
|
||||
func(_ context.Context, settings factory.ProviderSettings, cfg statementbuilder.Config) (qbtypes.StatementBuilder[qbtypes.TraceAggregation], error) {
|
||||
fm := tracestelemetryschema.NewFieldMapper(fl)
|
||||
cb := tracestelemetryschema.NewConditionBuilder(fm, fl)
|
||||
fm := tracestelemetryschema.NewFieldMapper()
|
||||
cb := tracestelemetryschema.NewConditionBuilder(fm)
|
||||
aggExprRewriter := querybuilder.NewAggExprRewriter(settings, nil, fm, cb, fl)
|
||||
return NewTraceQueryStatementBuilder(
|
||||
settings, metadataStore, fm, cb, aggExprRewriter, telemetryStore, fl,
|
||||
@@ -92,7 +91,6 @@ func NewTraceQueryStatementBuilder(
|
||||
cb: conditionBuilder,
|
||||
resourceFilterResolver: resourceFilterResolver,
|
||||
aggExprRewriter: aggExprRewriter,
|
||||
fl: flagger,
|
||||
skipResourceFingerprintEnabled: skipResourceFingerprintEnable,
|
||||
}
|
||||
}
|
||||
@@ -121,7 +119,7 @@ func (b *traceQueryStatementBuilder) Build(
|
||||
|
||||
// We modify SelectFields above (injecting default fields), and those default
|
||||
// fields can carry keys that need evolutions, so fetch keys after that.
|
||||
keySelectors := querybuilder.ExpandKeySelectorsForFamilies(ctx, orgID, b.fl, getKeySelectors(query))
|
||||
keySelectors := getKeySelectors(query)
|
||||
|
||||
keys, _, err := b.metadataStore.GetKeysMulti(ctx, orgID, keySelectors)
|
||||
if err != nil {
|
||||
@@ -797,7 +795,6 @@ func (b *traceQueryStatementBuilder) addFilterCondition(
|
||||
preparedWhereClause, err = querybuilder.PrepareWhereClause(query.Filter.Expression, querybuilder.FilterExprVisitorOpts{
|
||||
Context: ctx,
|
||||
OrgID: orgID,
|
||||
Flagger: b.fl,
|
||||
Logger: b.logger,
|
||||
FieldMapper: b.fm,
|
||||
ConditionBuilder: b.cb,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user