Compare commits

..

1 Commits

Author SHA1 Message Date
Ashwin Bhatkal
f43c21f5d9 ci(e2e): stop label events from failing the e2e check 2026-10-16 17:06:34 +05:30
100 changed files with 317 additions and 2947 deletions

View File

@@ -2,45 +2,45 @@ name: e2eci
on:
pull_request:
types:
- opened
- synchronize
- reopened
- labeled
pull_request_target:
types:
- labeled
merge_group:
types:
- checks_requested
concurrency:
group: e2eci-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
group: e2eci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
# Labels are read live, so a re-run (see e2elabelci) picks up a newly added
# safe-to-e2e or safe-to-test. Fork and dependabot PRs run only on a re-run:
# postci strips safe-to-test on push, and a first attempt could read the
# label before that lands.
changes:
if: github.event_name != 'merge_group'
runs-on: ubuntu-latest
outputs:
e2e: ${{ steps.changes.outputs.e2e }}
trusted: ${{ steps.changes.outputs.trusted }}
steps:
- name: changes
id: changes
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
LABELED: ${{ contains(github.event.pull_request.labels.*.name, 'safe-to-e2e') }}
FORK: ${{ github.event.pull_request.head.repo.fork }}
AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
labels=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/labels" --jq '.[].name')
files=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename')
if [ "$LABELED" = true ] || grep -qE '^(frontend|tests/e2e)/' <<< "$files"; then
if grep -qx safe-to-e2e <<< "$labels" || grep -qE '^(frontend|tests/e2e)/' <<< "$files"; then
echo "e2e=true" >> "$GITHUB_OUTPUT"
fi
if { [ "$FORK" = false ] && [ "$AUTHOR" != 'dependabot[bot]' ]; } || { [ "$GITHUB_RUN_ATTEMPT" -gt 1 ] && grep -qx safe-to-test <<< "$labels"; }; then
echo "trusted=true" >> "$GITHUB_OUTPUT"
fi
fmtlint:
needs: changes
if: |
((github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))) && needs.changes.outputs.e2e == 'true'
if: needs.changes.outputs.e2e == 'true' && needs.changes.outputs.trusted == 'true'
runs-on: ubuntu-latest
steps:
- name: checkout
@@ -70,9 +70,7 @@ jobs:
- chromium
shard: [1, 2, 3, 4]
needs: changes
if: |
((github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))) && needs.changes.outputs.e2e == 'true'
if: needs.changes.outputs.e2e == 'true' && needs.changes.outputs.trusted == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
@@ -193,5 +191,5 @@ jobs:
github.event_name != 'merge_group' && (needs.changes.result != 'success' ||
(needs.changes.outputs.e2e == 'true' && (needs.fmtlint.result != 'success' || needs.test.result != 'success')))
run: |
echo "::error::e2e did not pass. Fork and dependabot PRs need the safe-to-test label."
echo "::error::e2e did not pass. Fork and dependabot PRs need a maintainer to add the safe-to-test label."
exit 1

34
.github/workflows/e2elabelci.yaml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: e2elabelci
on:
pull_request_target:
types:
- labeled
permissions:
actions: write
jobs:
# Re-runs the commit's e2eci run instead of starting a second one, so the
# commit keeps a single e2e check. Never checks out PR code.
rerun:
if: github.event.label.name == 'safe-to-test' || github.event.label.name == 'safe-to-e2e'
runs-on: ubuntu-latest
steps:
- name: rerun
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
id=$(gh api "repos/$GITHUB_REPOSITORY/actions/workflows/e2eci.yaml/runs?event=pull_request&head_sha=$HEAD_SHA&per_page=1" --jq '.workflow_runs[0].id // empty')
if [ -z "$id" ]; then
echo "::error::No e2eci run found for $HEAD_SHA."
exit 1
fi
# A labeled re-run changes nothing once e2e has started.
started() {
[ "$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$id/jobs" --jq '[.jobs[] | select(.name == "fmtlint" and .conclusion != "skipped")] | length')" -gt 0 ]
}
started && exit 0
gh run watch "$id" --repo "$GITHUB_REPOSITORY" --interval 10 > /dev/null
started || gh run rerun "$id" --repo "$GITHUB_REPOSITORY"

View File

@@ -8046,31 +8046,6 @@ components:
- metric
- value
type: object
PromotetypesIndexMaterializedPathsResult:
properties:
indexed:
items:
$ref: '#/components/schemas/PromotetypesPromotePath'
nullable: true
type: array
skipped:
items:
$ref: '#/components/schemas/PromotetypesPromotePath'
nullable: true
type: array
required:
- indexed
- skipped
type: object
PromotetypesPostableIndexMaterializedPaths:
properties:
dryRun:
type: boolean
signal:
type: string
required:
- signal
type: object
PromotetypesPromotePath:
properties:
context:
@@ -14097,67 +14072,6 @@ paths:
tags:
- promote
x-signoz-stability: alpha
/api/v1/promoted_paths/materialized:
post:
deprecated: false
description: This endpoint indexes the JSON sub-columns of materialized paths
whose columns are indexed and returns the indexed and skipped paths.
operationId: IndexMaterializedPaths
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PromotetypesPostableIndexMaterializedPaths'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/PromotetypesIndexMaterializedPathsResult'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- logs-field:update
- traces-field:update
- tokenizer:
- logs-field:update
- traces-field:update
summary: Index materialized paths
tags:
- promote
x-signoz-stability: alpha
/api/v1/roles:
get:
deprecated: false
@@ -19000,9 +18914,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- metrics:read
- VIEWER
- tokenizer:
- metrics:read
- VIEWER
summary: Run Infra Monitoring Setup Checks
tags:
- inframonitoring
@@ -19074,9 +18988,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- metrics:read
- VIEWER
- tokenizer:
- metrics:read
- VIEWER
summary: List Clusters for Infra Monitoring
tags:
- inframonitoring
@@ -19158,9 +19072,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- metrics:read
- VIEWER
- tokenizer:
- metrics:read
- VIEWER
summary: List DaemonSets for Infra Monitoring
tags:
- inframonitoring
@@ -19236,9 +19150,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- metrics:read
- VIEWER
- tokenizer:
- metrics:read
- VIEWER
summary: List Deployments for Infra Monitoring
tags:
- inframonitoring
@@ -19304,9 +19218,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- metrics:read
- VIEWER
- tokenizer:
- metrics:read
- VIEWER
summary: List Hosts for Infra Monitoring
tags:
- inframonitoring
@@ -19386,9 +19300,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- metrics:read
- VIEWER
- tokenizer:
- metrics:read
- VIEWER
summary: List Jobs for Infra Monitoring
tags:
- inframonitoring
@@ -19467,9 +19381,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- metrics:read
- VIEWER
- tokenizer:
- metrics:read
- VIEWER
summary: List Kubernetes Containers for Infra Monitoring
tags:
- inframonitoring
@@ -19538,9 +19452,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- metrics:read
- VIEWER
- tokenizer:
- metrics:read
- VIEWER
summary: List Namespaces for Infra Monitoring
tags:
- inframonitoring
@@ -19613,9 +19527,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- metrics:read
- VIEWER
- tokenizer:
- metrics:read
- VIEWER
summary: List Nodes for Infra Monitoring
tags:
- inframonitoring
@@ -19689,9 +19603,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- metrics:read
- VIEWER
- tokenizer:
- metrics:read
- VIEWER
summary: List Pods for Infra Monitoring
tags:
- inframonitoring
@@ -19760,9 +19674,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- metrics:read
- VIEWER
- tokenizer:
- metrics:read
- VIEWER
summary: List Volumes for Infra Monitoring
tags:
- inframonitoring
@@ -19838,9 +19752,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- metrics:read
- VIEWER
- tokenizer:
- metrics:read
- VIEWER
summary: List StatefulSets for Infra Monitoring
tags:
- inframonitoring

View File

@@ -18,10 +18,8 @@ import type {
} from 'react-query';
import type {
IndexMaterializedPaths200,
ListPromotedPaths200,
ListPromotedPathsParams,
PromotetypesPostableIndexMaterializedPathsDTO,
PromotetypesPromotePathDTO,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
@@ -232,87 +230,3 @@ export const usePromotePaths = <
> => {
return useMutation(getPromotePathsMutationOptions(options));
};
/**
* This endpoint indexes the JSON sub-columns of materialized paths whose columns are indexed and returns the indexed and skipped paths.
* @summary Index materialized paths
*/
export const indexMaterializedPaths = (
promotetypesPostableIndexMaterializedPathsDTO?: BodyType<PromotetypesPostableIndexMaterializedPathsDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<IndexMaterializedPaths200>({
url: `/api/v1/promoted_paths/materialized`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: promotetypesPostableIndexMaterializedPathsDTO,
signal,
});
};
export const getIndexMaterializedPathsMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof indexMaterializedPaths>>,
TError,
{ data?: BodyType<PromotetypesPostableIndexMaterializedPathsDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof indexMaterializedPaths>>,
TError,
{ data?: BodyType<PromotetypesPostableIndexMaterializedPathsDTO> },
TContext
> => {
const mutationKey = ['indexMaterializedPaths'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof indexMaterializedPaths>>,
{ data?: BodyType<PromotetypesPostableIndexMaterializedPathsDTO> }
> = (props) => {
const { data } = props ?? {};
return indexMaterializedPaths(data);
};
return { mutationFn, ...mutationOptions };
};
export type IndexMaterializedPathsMutationResult = NonNullable<
Awaited<ReturnType<typeof indexMaterializedPaths>>
>;
export type IndexMaterializedPathsMutationBody =
| BodyType<PromotetypesPostableIndexMaterializedPathsDTO>
| undefined;
export type IndexMaterializedPathsMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Index materialized paths
*/
export const useIndexMaterializedPaths = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof indexMaterializedPaths>>,
TError,
{ data?: BodyType<PromotetypesPostableIndexMaterializedPathsDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof indexMaterializedPaths>>,
TError,
{ data?: BodyType<PromotetypesPostableIndexMaterializedPathsDTO> },
TContext
> => {
return useMutation(getIndexMaterializedPathsMutationOptions(options));
};

View File

@@ -9878,28 +9878,6 @@ export interface PromotetypesPromotePathDTO {
signal: string;
}
export interface PromotetypesIndexMaterializedPathsResultDTO {
/**
* @type array,null
*/
indexed: PromotetypesPromotePathDTO[] | null;
/**
* @type array,null
*/
skipped: PromotetypesPromotePathDTO[] | null;
}
export interface PromotetypesPostableIndexMaterializedPathsDTO {
/**
* @type boolean
*/
dryRun?: boolean;
/**
* @type string
*/
signal: string;
}
export interface Querybuildertypesv5AggregationMetaDTO {
/**
* @type array
@@ -13272,14 +13250,6 @@ export type ListPromotedPaths200 = {
status: string;
};
export type IndexMaterializedPaths200 = {
data: PromotetypesIndexMaterializedPathsResultDTO;
/**
* @type string
*/
status: string;
};
export type ListRoles200 = {
/**
* @type array

View File

@@ -61,11 +61,7 @@ describe('convertV5ResponseToLegacy', () => {
],
values: [
{ timestamp: 1000, value: 10 } as unknown as TimeSeriesValue,
{
timestamp: 2000,
value: 12,
partial: true,
} as unknown as TimeSeriesValue,
{ timestamp: 2000, value: 12 } as unknown as TimeSeriesValue,
],
} as unknown as TimeSeries,
],
@@ -108,7 +104,7 @@ describe('convertV5ResponseToLegacy', () => {
labels: { 'service.name': 'adservice' },
values: [
{ timestamp: 1000, value: '10' },
{ timestamp: 2000, value: '12', partial: true },
{ timestamp: 2000, value: '12' },
],
metaData: expect.objectContaining({
alias: '__result_0',

View File

@@ -125,7 +125,6 @@ function convertTimeSeriesData(
values: series.values.map((value: any) => ({
timestamp: value.timestamp,
value: String(value.value),
...(value.partial ? { partial: true } : {}),
})),
metaData: {
alias,

View File

@@ -1,3 +1,3 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#0E9AA7" d="M8.932 20.806c-.369 0-.738.007-1.109 0-.35-.007-.587-.206-.623-.5a.587.587 0 0 1 .53-.636c.79-.062 1.582-.063 2.372-.003a.548.548 0 0 1 .522.602c-.024.326-.253.526-.616.54zM1.792 8.345c-.392 0-.782.008-1.173.002-.327-.006-.577-.22-.614-.512-.037-.293.146-.544.499-.615.192-.032.388-.045.583-.039a81.515 81.515 0 0 1 1.597 0c.163 0 .325.019.483.056.288.073.445.318.411.617-.034.298-.214.477-.515.487-.424.014-.848.004-1.272.004zm7.588 8.417H4.292a2.464 2.464 0 0 1-.326-.007c-.294-.04-.48-.209-.508-.506-.029-.298.11-.501.391-.606.179-.065.365-.051.549-.051 3.347 0 6.695.005 10.042-.006 1.174-.004 2.187-.439 2.993-1.3.69-.738 1.053-1.63 1.16-2.635.085-.788-.027-1.513-.516-2.156-.544-.718-1.28-1.078-2.163-1.082-3.163-.013-6.328-.005-9.487-.01-.336 0-.673-.027-1.007-.058-.29-.027-.45-.201-.469-.492-.021-.317.141-.545.429-.6a1.55 1.55 0 0 1 .29-.015h10.177c1.71.004 3.187 1.038 3.726 2.654.383 1.147.246 2.304-.182 3.416-.824 2.135-2.762 3.448-5.055 3.454-1.652.005-3.304 0-4.956 0zm2.906-13.568c1.533 0 3.066-.008 4.598 0 2.935.018 5.629 1.892 6.653 4.626.442 1.181.538 2.403.412 3.657-.185 1.842-.735 3.552-1.776 5.084-1.608 2.365-3.873 3.68-6.679 4.118-.95.148-1.905.13-2.86.13-.397 0-.61-.181-.633-.51-.025-.351.196-.621.587-.645.434-.026.87-.004 1.305-.016 2.641-.072 4.928-.982 6.74-2.935 1.269-1.37 1.912-3.039 2.13-4.878.151-1.275.135-2.544-.37-3.752-.773-1.85-2.159-2.983-4.068-3.509-.74-.204-1.5-.243-2.26-.247-2.837-.017-5.675-.007-8.511-.007-.12 0-.24.004-.359-.006a.57.57 0 0 1-.517-.536.557.557 0 0 1 .456-.557c.13-.018.261-.024.392-.019h4.762Z"/>
<path fill="#29F1FB" d="M8.932 20.806c-.369 0-.738.007-1.109 0-.35-.007-.587-.206-.623-.5a.587.587 0 0 1 .53-.636c.79-.062 1.582-.063 2.372-.003a.548.548 0 0 1 .522.602c-.024.326-.253.526-.616.54zM1.792 8.345c-.392 0-.782.008-1.173.002-.327-.006-.577-.22-.614-.512-.037-.293.146-.544.499-.615.192-.032.388-.045.583-.039a81.515 81.515 0 0 1 1.597 0c.163 0 .325.019.483.056.288.073.445.318.411.617-.034.298-.214.477-.515.487-.424.014-.848.004-1.272.004zm7.588 8.417H4.292a2.464 2.464 0 0 1-.326-.007c-.294-.04-.48-.209-.508-.506-.029-.298.11-.501.391-.606.179-.065.365-.051.549-.051 3.347 0 6.695.005 10.042-.006 1.174-.004 2.187-.439 2.993-1.3.69-.738 1.053-1.63 1.16-2.635.085-.788-.027-1.513-.516-2.156-.544-.718-1.28-1.078-2.163-1.082-3.163-.013-6.328-.005-9.487-.01-.336 0-.673-.027-1.007-.058-.29-.027-.45-.201-.469-.492-.021-.317.141-.545.429-.6a1.55 1.55 0 0 1 .29-.015h10.177c1.71.004 3.187 1.038 3.726 2.654.383 1.147.246 2.304-.182 3.416-.824 2.135-2.762 3.448-5.055 3.454-1.652.005-3.304 0-4.956 0zm2.906-13.568c1.533 0 3.066-.008 4.598 0 2.935.018 5.629 1.892 6.653 4.626.442 1.181.538 2.403.412 3.657-.185 1.842-.735 3.552-1.776 5.084-1.608 2.365-3.873 3.68-6.679 4.118-.95.148-1.905.13-2.86.13-.397 0-.61-.181-.633-.51-.025-.351.196-.621.587-.645.434-.026.87-.004 1.305-.016 2.641-.072 4.928-.982 6.74-2.935 1.269-1.37 1.912-3.039 2.13-4.878.151-1.275.135-2.544-.37-3.752-.773-1.85-2.159-2.983-4.068-3.509-.74-.204-1.5-.243-2.26-.247-2.837-.017-5.675-.007-8.511-.007-.12 0-.24.004-.359-.006a.57.57 0 0 1-.517-.536.557.557 0 0 1 .456-.557c.13-.018.261-.024.392-.019h4.762Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1 +1,3 @@
<svg role="img" viewBox="-2.625 3 28 28" xmlns="http://www.w3.org/2000/svg"><path fill="#7A8291" d="M.36 8.6h16.7v5.6H6.04c-.2 0-.35.16-.35.35v4.9c0 .2.16.35.35.35h11.02v5.6h-5.33c-.2 0-.35.16-.35.35v4.9c0 .2.16.35.35.35h4.98c.2 0 .35-.15.35-.35V25.4h5.34c.2 0 .35-.16.35-.35v-4.9c0-.2-.16-.35-.35-.35h-5.34v-5.6h5.34c.2 0 .35-.16.35-.35v-4.9c0-.2-.16-.35-.35-.35h-5.34V3.35c0-.2-.16-.35-.35-.35H.36c-.2 0-.36.16-.36.35v4.9c0 .2.16.35.36.35Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="#9CA3AF" height="34" viewBox="0 0 131 34">
<path fill="#9CA3AF" d="M.36 8.6h16.7v5.6H6.04c-.2 0-.35.16-.35.35v4.9c0 .2.16.35.35.35h11.02v5.6h-5.33c-.2 0-.35.16-.35.35v4.9c0 .2.16.35.35.35h4.98c.2 0 .35-.15.35-.35V25.4h5.34c.2 0 .35-.16.35-.35v-4.9c0-.2-.16-.35-.35-.35h-5.34v-5.6h5.34c.2 0 .35-.16.35-.35v-4.9c0-.2-.16-.35-.35-.35h-5.34V3.35c0-.2-.16-.35-.35-.35H.36c-.2 0-.36.16-.36.35v4.9c0 .2.16.35.36.35ZM44.41 14.7c-.5-.5-1.1-.9-1.76-1.18a5.62 5.62 0 0 0-4.6.17c-.73.37-1.32.91-1.75 1.62h-.17V8.59H34.1v16.83h2.04v-1.81h.17c.21.36.47.67.77.94.31.25.65.48 1.01.67.37.18.77.31 1.18.39a6.2 6.2 0 0 0 3.39-.24 5.36 5.36 0 0 0 3.02-3.1c.29-.75.44-1.62.44-2.6v-.47c0-.96-.16-1.83-.47-2.58-.3-.75-.7-1.4-1.23-1.9v-.01Zm-5.87.66a3.9 3.9 0 0 1 4.34.84c.36.35.64.8.83 1.3.2.5.3 1.07.3 1.7v.47c0 .64-.1 1.23-.3 1.74a3.75 3.75 0 0 1-2.06 2.15 4.27 4.27 0 0 1-3.12-.03 3.86 3.86 0 0 1-2.09-2.2c-.2-.52-.3-1.11-.3-1.75v-.29c0-.62.1-1.2.3-1.7v-.01c.21-.53.5-.99.84-1.36.36-.37.78-.66 1.26-.86ZM97.04 8.59H95v4.86h-2.94v1.86H95v8.17c0 .56.17 1.03.53 1.4.37.35.84.54 1.4.54h4.18v-1.87h-3.5c-.2 0-.33-.05-.43-.15-.1-.1-.14-.27-.14-.49v-7.6h4.65v-1.86h-4.65V8.59ZM114.61 15a5.48 5.48 0 0 0-1.8-1.33 5.6 5.6 0 0 0-2.57-.56 6.17 6.17 0 0 0-4.26 1.7 5.6 5.6 0 0 0-1.72 4.2v.57c0 .9.15 1.75.44 2.5a5.58 5.58 0 0 0 5.5 3.67c1.55 0 2.8-.35 3.72-1.04a5.35 5.35 0 0 0 1.91-2.73l.03-.07-1.94-.52-.02.07c-.11.33-.27.64-.46.94-.17.27-.4.52-.7.74-.28.22-.63.39-1.04.51-.41.13-.9.19-1.46.19a3.8 3.8 0 0 1-2.84-1.05 4.07 4.07 0 0 1-1.1-2.7h9.68v-1.6c0-.54-.11-1.12-.34-1.75a5.04 5.04 0 0 0-1.03-1.74Zm-8.25 3.21a3.8 3.8 0 0 1 1.22-2.25 4.19 4.19 0 0 1 3.99-.7c.44.16.83.38 1.17.66.34.27.62.62.82 1.02.21.38.34.8.38 1.27h-7.58ZM129.09 14.42a4.47 4.47 0 0 0-3.37-1.3c-.93 0-1.73.2-2.4.59-.64.39-1.15.97-1.52 1.74h-.17v-2h-2.04v11.97h2.04v-6.23c0-1.26.32-2.28.95-3.02a3.31 3.31 0 0 1 2.65-1.14c.94 0 1.7.3 2.24.9.56.6.83 1.52.83 2.74v6.75h2.04v-7.13c0-1.71-.42-3.02-1.25-3.87ZM88.1 15a5.48 5.48 0 0 0-1.78-1.33 5.6 5.6 0 0 0-2.58-.56 6.17 6.17 0 0 0-4.27 1.7 5.59 5.59 0 0 0-1.71 4.2v.56c0 .92.14 1.76.44 2.51a5.6 5.6 0 0 0 5.5 3.67c1.55 0 2.8-.35 3.72-1.04a5.36 5.36 0 0 0 1.91-2.73l.03-.07-1.94-.52-.03.07c-.1.32-.26.64-.45.94-.17.27-.4.52-.7.74-.29.21-.64.39-1.05.51-.4.12-.9.19-1.45.19a3.8 3.8 0 0 1-2.85-1.05 4.07 4.07 0 0 1-1.09-2.7h9.68v-1.61c0-.53-.12-1.12-.34-1.74A5.03 5.03 0 0 0 88.1 15Zm-8.24 3.21a3.83 3.83 0 0 1 1.22-2.25 4.2 4.2 0 0 1 3.99-.7c.44.16.83.38 1.16.66.35.27.62.62.83 1.02.2.38.33.8.37 1.27h-7.57ZM73.65 19.42a6.11 6.11 0 0 0-3.23-1.02 6.63 6.63 0 0 1-2.68-.58c-.47-.3-.7-.7-.7-1.25 0-.27.08-.5.21-.7.14-.2.33-.38.56-.52a4.05 4.05 0 0 1 1.78-.42c.85 0 1.54.21 2.06.63.53.41.83 1 .91 1.73l.01.1 1.95-.47-.01-.07c-.07-.45-.22-.91-.45-1.36a3.46 3.46 0 0 0-.94-1.2 4.6 4.6 0 0 0-1.52-.84 6.05 6.05 0 0 0-2.1-.34c-.58 0-1.13.07-1.65.22-.52.14-1 .36-1.43.65-.41.3-.74.66-.99 1.1a2.9 2.9 0 0 0-.37 1.49v.14c0 1.06.38 1.87 1.14 2.42a6.2 6.2 0 0 0 3.24.97c1.18.08 2.04.26 2.56.54.5.28.75.72.75 1.36 0 .6-.25 1.06-.78 1.4-.52.32-1.22.48-2.1.48a3.68 3.68 0 0 1-2.46-.79 3.13 3.13 0 0 1-1.04-2.14v-.09l-1.93.46h-.02v.07a4.4 4.4 0 0 0 3.1 4c.7.24 1.52.36 2.46.36.7 0 1.35-.09 1.93-.27.6-.16 1.12-.4 1.53-.72A3.38 3.38 0 0 0 74.8 22v-.14c0-1.07-.39-1.9-1.14-2.44ZM60.25 23.4c-.1-.1-.14-.27-.14-.49v-9.46h-2.05v1.85h-.16a3.78 3.78 0 0 0-1.61-1.63 4.62 4.62 0 0 0-2.26-.56c-.77 0-1.5.14-2.19.41a5.27 5.27 0 0 0-3.02 3.12c-.29.75-.44 1.63-.44 2.6v.38c0 .99.15 1.87.44 2.63.3.75.7 1.4 1.2 1.93a5.48 5.48 0 0 0 4.05 1.57c.8 0 1.51-.2 2.2-.58.69-.38 1.24-.97 1.63-1.75h.16v.06c0 .56.18 1.03.53 1.4.37.35.85.54 1.41.54h1.36v-1.87h-.68c-.2 0-.34-.05-.43-.15Zm-4.46.13c-.46.2-.97.3-1.52.3a3.68 3.68 0 0 1-2.75-1.09 4.42 4.42 0 0 1-1.05-3.12v-.38c0-.62.1-1.2.29-1.71a3.65 3.65 0 0 1 5-2.17c.47.2.88.49 1.21.86.35.37.62.83.8 1.36.2.5.3 1.08.3 1.7v.3c0 .63-.1 1.23-.3 1.76-.18.5-.45.96-.78 1.33-.33.37-.73.66-1.2.86Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 451 B

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="#37814A" d="M2.303 0A2.298 2.298 0 0 0 0 2.303v19.394A2.298 2.298 0 0 0 2.303 24h19.394A2.298 2.298 0 0 0 24 21.697V2.303A2.298 2.298 0 0 0 21.697 0zm8.177 3.072c4.098 0 7.028 1.438 7.68 1.764l-1.194 2.55c-2.442-1.057-4.993-1.41-5.672-1.41-1.574 0-2.17.922-2.17 1.763v8.494c0 .869.596 1.791 2.17 1.791.679 0 3.23-.38 5.672-1.41l1.194 2.496c-.435.271-3.637 1.818-7.68 1.818-1.112 0-4.64-.244-4.64-4.64V7.713c0-4.397 3.528-4.64 4.64-4.64z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" id="图层_1" x="0" y="0" version="1.1" viewBox="0 0 519.27 214.14"><style>.st2{fill:#a9cc54}</style><path d="M493.54 171.25c-35.63 0-64.52-28.89-64.52-64.52s28.89-64.52 64.52-64.52c3.85 0 7.62.36 11.29 1L493.54 5.45H104.46v.02C49.24 6.51 4.78 51.59 4.78 107.07c0 55.47 44.45 100.56 99.68 101.59v.02h389.07l11.29-38.44c-3.66.65-7.43 1.01-11.28 1.01" style="fill:#b6de64"/><defs><path id="SVGID_1_" d="M493.54 171.25c-35.63 0-64.52-28.89-64.52-64.52s28.89-64.52 64.52-64.52c3.85 0 7.62.36 11.29 1V5.45H104.46v.02C49.24 6.51 4.78 51.59 4.78 107.07c0 55.47 44.45 100.56 99.68 101.59v.02h389.07l11.29-38.44c-3.66.65-7.43 1.01-11.28 1.01"/></defs><clipPath id="SVGID_2_"><use xlink:href="#SVGID_1_" style="overflow:visible"/></clipPath><g style="clip-path:url(#SVGID_2_)"><path d="M-23.16 55.58H428.5v51.1H-23.16zM-23.16 157.59h530.98v72.35H-23.16z" class="st2"/></g><path d="M493.54 184.23c-43.94 0-79.56-33.62-79.56-77.56s35.62-76.56 79.56-76.56" style="fill:none;stroke:#ddf4a4;stroke-width:49;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10"/></svg>

Before

Width:  |  Height:  |  Size: 529 B

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -1,5 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="lucide/github">
<path id="Vector" d="M15 22V18C15.1391 16.7473 14.7799 15.4901 14 14.5C17 14.5 20 12.5 20 9C20.08 7.75 19.73 6.52 19 5.5C19.28 4.35 19.28 3.15 19 2C19 2 18 2 16 3.5C13.36 3 10.64 3 8 3.5C6 2 5 2 5 2C4.7 3.15 4.7 4.35 5 5.5C4.27187 6.51588 3.91847 7.75279 4 9C4 12.5 7 14.5 10 14.5C9.61 14.99 9.32 15.55 9.15 16.15C8.98 16.75 8.93 17.38 9 18M9 18V22M9 18C4.49 20 4 16 2 16" stroke="#7A8291" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector" d="M15 22V18C15.1391 16.7473 14.7799 15.4901 14 14.5C17 14.5 20 12.5 20 9C20.08 7.75 19.73 6.52 19 5.5C19.28 4.35 19.28 3.15 19 2C19 2 18 2 16 3.5C13.36 3 10.64 3 8 3.5C6 2 5 2 5 2C4.7 3.15 4.7 4.35 5 5.5C4.27187 6.51588 3.91847 7.75279 4 9C4 12.5 7 14.5 10 14.5C9.61 14.99 9.32 15.55 9.15 16.15C8.98 16.75 8.93 17.38 9 18M9 18V22M9 18C4.49 20 4 16 2 16" stroke="#C0C1C3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 587 B

After

Width:  |  Height:  |  Size: 587 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#A68400" d="m0 0 11.955 24L24 0zm13.366 4.827h1.393v1.38h-1.393zm-2.77 5.569H9.22V8.993h1.389zm0-2.087H9.22V6.906h1.389zm0-2.086H9.22V4.819h1.389zm2.087 6.263h-1.377V11.08h1.388zm0-2.09h-1.377V8.993h1.388zm0-2.087h-1.377V6.906h1.388zm0-2.086h-1.377V4.819h1.388zm.683.683h1.393v1.389h-1.393zm0 3.475V8.993h1.389v1.388Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#FFEC6E" d="m0 0 11.955 24L24 0zm13.366 4.827h1.393v1.38h-1.393zm-2.77 5.569H9.22V8.993h1.389zm0-2.087H9.22V6.906h1.389zm0-2.086H9.22V4.819h1.389zm2.087 6.263h-1.377V11.08h1.388zm0-2.09h-1.377V8.993h1.388zm0-2.087h-1.377V6.906h1.388zm0-2.086h-1.377V4.819h1.388zm.683.683h1.393v1.389h-1.393zm0 3.475V8.993h1.389v1.388Z"/></svg>

Before

Width:  |  Height:  |  Size: 398 B

After

Width:  |  Height:  |  Size: 398 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="285" height="285" preserveAspectRatio="xMidYMid" viewBox="-14.5 0 285 285"><path fill="#7C4DD6" d="M230.4 0c13.996 0 25.37 11.232 25.597 25.177l.003.423v233.244c0 13.996-11.235 25.37-25.177 25.597l-.423.003H25.6c-13.996 0-25.37-11.235-25.597-25.176L0 258.844V25.6C0 11.601 11.235.23 25.177.003L25.6 0zm0 14.222H25.6c-6.174 0-11.218 4.948-11.374 11.085l-.004.293v233.244c0 6.174 4.948 11.218 11.085 11.374l.293.004h204.8c6.177 0 11.218-4.947 11.374-11.084l.004-.294V25.6c0-6.272-5.102-11.378-11.378-11.378M64 184.89l32 28.444-32 28.445zM92.444 42.667v80.75c14.201-4.622 34.052-9.64 53.334-9.64 17.582 0 28.106 6.913 33.838 12.712 12.109 12.25 12.397 27.82 12.381 29.897v.058l.003 85.334h-28.444v-84.942c-.14-6.639-3.34-14.614-17.778-14.614-28.632 0-60.714 14.257-61.666 14.683l-.02.009L64 166.016V42.666zm99.556 0c-1.92 16.156-8.47 31.644-21.333 46.222h-28.445c11.179-14.667 18.198-30.101 21.334-46.222Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="285" height="285" preserveAspectRatio="xMidYMid" viewBox="-14.5 0 285 285"><path fill="#430098" d="M230.4 0c13.996 0 25.37 11.232 25.597 25.177l.003.423v233.244c0 13.996-11.235 25.37-25.177 25.597l-.423.003H25.6c-13.996 0-25.37-11.235-25.597-25.176L0 258.844V25.6C0 11.601 11.235.23 25.177.003L25.6 0zm0 14.222H25.6c-6.174 0-11.218 4.948-11.374 11.085l-.004.293v233.244c0 6.174 4.948 11.218 11.085 11.374l.293.004h204.8c6.177 0 11.218-4.947 11.374-11.084l.004-.294V25.6c0-6.272-5.102-11.378-11.378-11.378M64 184.89l32 28.444-32 28.445zM92.444 42.667v80.75c14.201-4.622 34.052-9.64 53.334-9.64 17.582 0 28.106 6.913 33.838 12.712 12.109 12.25 12.397 27.82 12.381 29.897v.058l.003 85.334h-28.444v-84.942c-.14-6.639-3.34-14.614-17.778-14.614-28.632 0-60.714 14.257-61.666 14.683l-.02.009L64 166.016V42.666zm99.556 0c-1.92 16.156-8.47 31.644-21.333 46.222h-28.445c11.179-14.667 18.198-30.101 21.334-46.222Z"/></svg>

Before

Width:  |  Height:  |  Size: 958 B

After

Width:  |  Height:  |  Size: 958 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 463 B

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -1 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-4v4h4v-4zM18 6h-4v4.001h4v-4zM18 14h-4v4h4v-4zM22 2h-4v4h4V2zM22 18h-4v4h4v-4z" fill="#1FD5F9"></path><path d="M6 18V2H2v20h12v-4H6z" fill="#7A8291"></path></svg>
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>LiveKit</title><path d="M14 10h-4v4h4v-4zM18 6h-4v4.001h4v-4zM18 14h-4v4h4v-4zM22 2h-4v4h4V2zM22 18h-4v4h4v-4z" fill="#1FD5F9"></path><path d="M6 18V2H2v20h12v-4H6z" fill="#fff"></path></svg>

Before

Width:  |  Height:  |  Size: 251 B

After

Width:  |  Height:  |  Size: 316 B

View File

@@ -1 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="#7A8291" d="M22 24H2V0h20zM17 4.8H7v14.4h10z"/></svg>
<svg width='234' height='42' viewBox='0 0 234 42' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M18 30H6V18H18V30Z' fill='#CFCECD'/><path d='M18 12H6V30H18V12ZM24 36H0V6H24V36Z' fill='#656363'/><path d='M48 30H36V18H48V30Z' fill='#CFCECD'/><path d='M36 30H48V12H36V30ZM54 36H36V42H30V6H54V36Z' fill='#656363'/><path d='M84 24V30H66V24H84Z' fill='#CFCECD'/><path d='M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z' fill='#656363'/><path d='M108 36H96V18H108V36Z' fill='#CFCECD'/><path d='M108 12H96V36H90V6H108V12ZM114 36H108V12H114V36Z' fill='#656363'/><path d='M144 30H126V18H144V30Z' fill='#CFCECD'/><path d='M144 12H126V30H144V36H120V6H144V12Z' fill='#211E1E'/><path d='M168 30H156V18H168V30Z' fill='#CFCECD'/><path d='M168 12H156V30H168V12ZM174 36H150V6H174V36Z' fill='#211E1E'/><path d='M198 30H186V18H198V30Z' fill='#CFCECD'/><path d='M198 12H186V30H198V12ZM204 36H180V6H198V0H204V36Z' fill='#211E1E'/><path d='M234 24V30H216V24H234Z' fill='#CFCECD'/><path d='M216 12V18H228V12H216ZM234 24H216V30H234V36H210V6H234V24Z' fill='#211E1E'/></svg>

Before

Width:  |  Height:  |  Size: 137 B

After

Width:  |  Height:  |  Size: 1.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -1 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="#7A8291" d="M18.263.007c-3.121-.147-5.744 2.109-6.192 5.082-.018.138-.045.272-.067.405-.696 3.703-3.936 6.507-7.827 6.507-1.388 0-2.691-.356-3.825-.979a.2024.2024 0 0 0-.302.178V24H12v-8.999c0-1.656 1.338-3 2.987-3h2.988c3.382 0 6.103-2.817 5.97-6.244-.12-3.084-2.61-5.603-5.682-5.75"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Render</title><path d="M18.263.007c-3.121-.147-5.744 2.109-6.192 5.082-.018.138-.045.272-.067.405-.696 3.703-3.936 6.507-7.827 6.507a7.9 7.9 0 0 1-3.825-.979.202.202 0 0 0-.302.178V24H12v-8.999c0-1.656 1.338-3 2.987-3h2.988c3.382 0 6.103-2.817 5.97-6.244-.12-3.084-2.61-5.603-5.682-5.75"/></svg>

Before

Width:  |  Height:  |  Size: 376 B

After

Width:  |  Height:  |  Size: 362 B

View File

@@ -1 +1 @@
<svg role="img" viewBox="89.7 190 220 220" xmlns="http://www.w3.org/2000/svg"><linearGradient id="a" x1="128.046" x2="313.226" y1="579.207" y2="472.293" gradientTransform="translate(-20.96 -225.764)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#10e7ff"/><stop offset=".103" style="stop-color:#33d5ff"/><stop offset=".255" style="stop-color:#60bdff"/><stop offset=".408" style="stop-color:#86aaff"/><stop offset=".56" style="stop-color:#a39aff"/><stop offset=".71" style="stop-color:#b790ff"/><stop offset=".858" style="stop-color:#c489ff"/><stop offset="1" style="stop-color:#c887ff"/></linearGradient><path d="m199.7 408.2-92.2-54.1V245.9l92.2-54.1 92.2 54.1v108.2zm-79.4-61.7 79.1 46.5 79.1-46.5v-92.9l-79-46.5-79.3 46.5c.1-.1.1 92.9.1 92.9" style="fill:url(#a)"/><linearGradient id="b" x1="185.354" x2="277.202" y1="542.013" y2="488.984" gradientTransform="translate(-20.96 -225.764)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#10e7ff"/><stop offset=".103" style="stop-color:#33d5ff"/><stop offset=".255" style="stop-color:#60bdff"/><stop offset=".408" style="stop-color:#86aaff"/><stop offset=".56" style="stop-color:#a39aff"/><stop offset=".71" style="stop-color:#b790ff"/><stop offset=".858" style="stop-color:#c489ff"/><stop offset="1" style="stop-color:#c887ff"/></linearGradient><path d="m239.4 270.6-32.3 56.8 10.5 18.5 42.8-75.3zm-36.1 0L171 327.4l10.5 18.5 42.8-75.3zm-64.4 0 42.8 75.3 10.5-18.5-32.3-56.8zm0 0 42.8 75.3 10.5-18.5-32.3-56.8zm64.4 0L171 327.4l10.5 18.5 42.8-75.3z" style="fill:url(#b)"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" version="1.0" viewBox="0 0 800 600"><path d="M364.7 246.1h16.9L363.2 300h-16.9l-18.5-53.9h17.1l10.2 33h.2zM436 300h-39.4v-53.9h39.2v13.6h-23.6v7.1h20.9V279h-20.9v7.4H436zm41.6-54.8c11.3 0 19.6 5.8 22.1 15.1l-14.7 6.4c-.7-4.2-2.9-7.4-7.6-7.4-5.3 0-9.3 4.4-9.3 14s3.8 14.2 9.3 14.2c4.5 0 6.9-3.4 7.6-7.6l14.7 6.4c-2.5 9.3-10.9 15.1-22.1 15.1-16.2 0-26.1-10.7-26.1-28s9.9-28.2 26.1-28.2m50.3 54.8v-40.3h-16.2v-13.6h48.5v13.6H544V300zm97.7-26.9c0 17.2-10 28-26.1 28-16 0-26.1-10.7-26.1-28s10.2-28 26.1-28c16.1.1 26.1 10.8 26.1 28m-35.4 0c0 8.7 3.3 14.2 9.4 14.2s9.4-5.3 9.4-14.2c0-8.7-3.3-14.2-9.4-14.2s-9.4 5.3-9.4 14.2m84.4 26.9c-1.5-1.8-2-3.4-2.5-6.4l-1.1-6c-.7-3.8-2.5-4.9-5.8-4.9h-6.4V300h-16v-53.9h25.6c12.2 0 20.7 4.7 20.7 16.5 0 7.8-5.3 12.3-11.1 13.4v.2c4.7.7 7.4 3.6 8.5 8.5l2 8.5c.7 2.9 1.6 4.7 3.4 6.7.1.1-17.3.1-17.3.1m-8.2-40.7h-7.8V270h7.8c3.8 0 5.8-1.6 5.8-5.4 0-3.5-1.8-5.3-5.8-5.3m-321.5 81.2c3.3.4 5.6 2.4 5.6 6 0 4.4-2.7 7.3-8.7 7.3h-9.3v-26.1h8.9c5.1 0 8.2 2.4 8.2 6.9 0 3.1-2 5.3-4.7 5.9m-9.1-1.1h5.4c3.3 0 5.1-1.5 5.1-4.5s-1.8-4.5-5.1-4.5h-5.4zm0 11.6h5.8c3.6 0 5.4-1.6 5.4-4.5s-2-4.5-5.4-4.5h-5.8zm28-10.7 7.1-12.7h3.4l-8.9 15.8v10.2h-3.3v-10.2l-9.1-15.8h3.6zm26.7-12.7h7.6c7.3 0 12.7 4 12.7 12.9s-5.4 13.1-12.7 13.1h-7.6zm3.2 23.2h4.4c4.7 0 9.3-2.5 9.3-10.3s-4.5-10.2-9.3-10.2h-4.4zm36.2-4.3H419l-2.5 7.3h-3.3l9.4-26.1h3.6l9.4 26.1h-3.3zm-10-2.9h8.9l-4.4-12.5zm24.1 10.1v-23.2h-8.7v-2.9h20.9v2.9h-8.7v23.2zm28.3-7.2h-10.9l-2.5 7.3h-3.3l9.4-26.1h3.6l9.4 26.1h-3.3zm-9.8-2.9h8.9l-4.4-12.5zm20.7-16h7.6c7.3 0 12.7 4 12.7 12.9s-5.4 13.1-12.7 13.1h-7.6zm3.3 23.2h4.4c4.7 0 9.3-2.5 9.3-10.3s-4.5-10.2-9.3-10.2h-4.4zm44.1-10.1c0 8.4-4.5 13.4-11.3 13.4S508 349 508 340.7c0-8.4 4.5-13.4 11.3-13.4s11.3 5 11.3 13.4m-19 0c0 6.4 2.7 10.7 7.8 10.7s7.8-4.4 7.8-10.7c0-6.4-2.7-10.7-7.8-10.7s-7.8 4.3-7.8 10.7m45-.2v13.1H554l-.5-3.8c-.9 2.7-3.8 4.4-7.3 4.4-6.5 0-11.1-4.9-11.1-13.4 0-8.7 4.9-13.6 11.4-13.6 4.7 0 8 2 9.4 6l-2.9 1.6c-.9-3.1-3.4-4.9-6.7-4.9-5.1 0-8 4-8 10.7 0 6.4 2.5 10.7 8 10.7 3.4 0 6.7-1.8 6.7-6.5V343h-6.4v-2.7h9.8z"/><linearGradient id="a" x1="128.046" x2="313.226" y1="579.207" y2="472.293" gradientTransform="translate(-20.96 -225.764)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#10e7ff"/><stop offset=".103" style="stop-color:#33d5ff"/><stop offset=".255" style="stop-color:#60bdff"/><stop offset=".408" style="stop-color:#86aaff"/><stop offset=".56" style="stop-color:#a39aff"/><stop offset=".71" style="stop-color:#b790ff"/><stop offset=".858" style="stop-color:#c489ff"/><stop offset="1" style="stop-color:#c887ff"/></linearGradient><path d="m199.7 408.2-92.2-54.1V245.9l92.2-54.1 92.2 54.1v108.2zm-79.4-61.7 79.1 46.5 79.1-46.5v-92.9l-79-46.5-79.3 46.5c.1-.1.1 92.9.1 92.9" style="fill:url(#a)"/><linearGradient id="b" x1="185.354" x2="277.202" y1="542.013" y2="488.984" gradientTransform="translate(-20.96 -225.764)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#10e7ff"/><stop offset=".103" style="stop-color:#33d5ff"/><stop offset=".255" style="stop-color:#60bdff"/><stop offset=".408" style="stop-color:#86aaff"/><stop offset=".56" style="stop-color:#a39aff"/><stop offset=".71" style="stop-color:#b790ff"/><stop offset=".858" style="stop-color:#c489ff"/><stop offset="1" style="stop-color:#c887ff"/></linearGradient><path d="m239.4 270.6-32.3 56.8 10.5 18.5 42.8-75.3zm-36.1 0L171 327.4l10.5 18.5 42.8-75.3zm-64.4 0 42.8 75.3 10.5-18.5-32.3-56.8zm0 0 42.8 75.3 10.5-18.5-32.3-56.8zm64.4 0L171 327.4l10.5 18.5 42.8-75.3z" style="fill:url(#b)"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -560,7 +560,7 @@ const onboardingConfigWithLinks = [
key: 'springboot',
label: 'Spring Boot',
imgUrl: springbootUrl,
link: '/docs/instrumentation/java/opentelemetry-java/',
link: '/docs/instrumentation/opentelemetry-springboot/',
question: {
desc: 'What is your Environment?',
type: 'select',
@@ -570,25 +570,25 @@ const onboardingConfigWithLinks = [
key: 'vm',
label: 'VM',
imgUrl: vmUrl,
link: '/docs/instrumentation/java/opentelemetry-java/',
link: '/docs/instrumentation/opentelemetry-springboot/',
},
{
key: 'k8s',
label: 'Kubernetes',
imgUrl: kubernetesUrl,
link: '/docs/instrumentation/java/opentelemetry-java/',
link: '/docs/instrumentation/opentelemetry-springboot/',
},
{
key: 'docker',
label: 'Docker',
imgUrl: dockerUrl,
link: '/docs/instrumentation/java/opentelemetry-java/',
link: '/docs/instrumentation/opentelemetry-springboot/',
},
{
key: 'windows',
label: 'Windows',
imgUrl: windowsUrl,
link: '/docs/instrumentation/java/opentelemetry-java/',
link: '/docs/instrumentation/opentelemetry-springboot/',
},
],
},
@@ -597,7 +597,7 @@ const onboardingConfigWithLinks = [
key: 'tomcat',
label: 'Tomcat',
imgUrl: tomcatUrl,
link: '/docs/instrumentation/java/opentelemetry-tomcat/',
link: '/docs/instrumentation/opentelemetry-tomcat/',
question: {
desc: 'What is your Environment?',
type: 'select',
@@ -607,25 +607,25 @@ const onboardingConfigWithLinks = [
key: 'vm',
label: 'VM',
imgUrl: vmUrl,
link: '/docs/instrumentation/java/opentelemetry-tomcat/',
link: '/docs/instrumentation/opentelemetry-tomcat/',
},
{
key: 'k8s',
label: 'Kubernetes',
imgUrl: kubernetesUrl,
link: '/docs/instrumentation/java/opentelemetry-tomcat/',
link: '/docs/instrumentation/opentelemetry-tomcat/',
},
{
key: 'docker',
label: 'Docker',
imgUrl: dockerUrl,
link: '/docs/instrumentation/java/opentelemetry-tomcat/',
link: '/docs/instrumentation/opentelemetry-tomcat/',
},
{
key: 'windows',
label: 'Windows',
imgUrl: windowsUrl,
link: '/docs/instrumentation/java/opentelemetry-tomcat/',
link: '/docs/instrumentation/opentelemetry-tomcat/',
},
],
},
@@ -634,7 +634,7 @@ const onboardingConfigWithLinks = [
key: 'jboss',
label: 'JBoss/WildFly',
imgUrl: jbossUrl,
link: '/docs/instrumentation/java/opentelemetry-jboss/',
link: '/docs/instrumentation/opentelemetry-jboss/',
question: {
desc: 'What is your Environment?',
type: 'select',
@@ -644,25 +644,25 @@ const onboardingConfigWithLinks = [
key: 'vm',
label: 'VM',
imgUrl: vmUrl,
link: '/docs/instrumentation/java/opentelemetry-jboss/',
link: '/docs/instrumentation/opentelemetry-jboss/',
},
{
key: 'k8s',
label: 'Kubernetes',
imgUrl: kubernetesUrl,
link: '/docs/instrumentation/java/opentelemetry-jboss/',
link: '/docs/instrumentation/opentelemetry-jboss/',
},
{
key: 'docker',
label: 'Docker',
imgUrl: dockerUrl,
link: '/docs/instrumentation/java/opentelemetry-jboss/',
link: '/docs/instrumentation/opentelemetry-jboss/',
},
{
key: 'windows',
label: 'Windows',
imgUrl: windowsUrl,
link: '/docs/instrumentation/java/opentelemetry-jboss/',
link: '/docs/instrumentation/opentelemetry-jboss/',
},
],
},
@@ -671,7 +671,7 @@ const onboardingConfigWithLinks = [
key: 'opentelemetry-quarkus',
label: 'Quarkus',
imgUrl: quarkusUrl,
link: '/docs/instrumentation/java/opentelemetry-quarkus/',
link: '/docs/instrumentation/opentelemetry-quarkus/',
question: {
desc: 'What is your Environment?',
type: 'select',
@@ -681,25 +681,25 @@ const onboardingConfigWithLinks = [
key: 'vm',
label: 'VM',
imgUrl: vmUrl,
link: '/docs/instrumentation/java/opentelemetry-quarkus/',
link: '/docs/instrumentation/opentelemetry-quarkus/',
},
{
key: 'k8s',
label: 'Kubernetes',
imgUrl: kubernetesUrl,
link: '/docs/instrumentation/java/opentelemetry-quarkus/',
link: '/docs/instrumentation/opentelemetry-quarkus/',
},
{
key: 'docker',
label: 'Docker',
imgUrl: dockerUrl,
link: '/docs/instrumentation/java/opentelemetry-quarkus/',
link: '/docs/instrumentation/opentelemetry-quarkus/',
},
{
key: 'windows',
label: 'Windows',
imgUrl: windowsUrl,
link: '/docs/instrumentation/java/opentelemetry-quarkus/',
link: '/docs/instrumentation/opentelemetry-quarkus/',
},
],
},
@@ -708,7 +708,7 @@ const onboardingConfigWithLinks = [
key: 'Others',
label: 'Others',
imgUrl: javaOthersUrl,
link: '/docs/instrumentation/java/opentelemetry-java/',
link: '/docs/instrumentation/opentelemetry-java/',
question: {
desc: 'What is your Environment?',
type: 'select',
@@ -718,25 +718,25 @@ const onboardingConfigWithLinks = [
key: 'vm',
label: 'VM',
imgUrl: vmUrl,
link: '/docs/instrumentation/java/opentelemetry-java/',
link: '/docs/instrumentation/opentelemetry-java/',
},
{
key: 'k8s',
label: 'Kubernetes',
imgUrl: kubernetesUrl,
link: '/docs/instrumentation/java/opentelemetry-java/',
link: '/docs/instrumentation/opentelemetry-java/',
},
{
key: 'docker',
label: 'Docker',
imgUrl: dockerUrl,
link: '/docs/instrumentation/java/opentelemetry-java/',
link: '/docs/instrumentation/opentelemetry-java/',
},
{
key: 'windows',
label: 'Windows',
imgUrl: windowsUrl,
link: '/docs/instrumentation/java/opentelemetry-java/',
link: '/docs/instrumentation/opentelemetry-java/',
},
],
},
@@ -872,7 +872,7 @@ const onboardingConfigWithLinks = [
key: 'nodejs-nestjs',
label: 'NodeJs/NestJS',
imgUrl: nodejsUrl,
link: '/docs/instrumentation/javascript/opentelemetry-nodejs/',
link: '/docs/instrumentation/opentelemetry-javascript/',
question: {
desc: 'What is your Environment?',
type: 'select',
@@ -882,25 +882,25 @@ const onboardingConfigWithLinks = [
key: 'vm',
label: 'VM',
imgUrl: vmUrl,
link: '/docs/instrumentation/javascript/opentelemetry-nodejs/',
link: '/docs/instrumentation/opentelemetry-javascript/',
},
{
key: 'k8s',
label: 'Kubernetes',
imgUrl: kubernetesUrl,
link: '/docs/instrumentation/javascript/opentelemetry-nodejs/',
link: '/docs/instrumentation/opentelemetry-javascript/',
},
{
key: 'docker',
label: 'Docker',
imgUrl: dockerUrl,
link: '/docs/instrumentation/javascript/opentelemetry-nodejs/',
link: '/docs/instrumentation/opentelemetry-javascript/',
},
{
key: 'windows',
label: 'Windows',
imgUrl: windowsUrl,
link: '/docs/instrumentation/javascript/opentelemetry-nodejs/',
link: '/docs/instrumentation/opentelemetry-javascript/',
},
],
},
@@ -909,7 +909,7 @@ const onboardingConfigWithLinks = [
key: 'nextjs',
label: 'NextJS',
imgUrl: nextjsUrl,
link: '/docs/instrumentation/javascript/opentelemetry-nextjs/',
link: '/docs/instrumentation/opentelemetry-nextjs/',
question: {
desc: 'What is your Environment?',
type: 'select',
@@ -919,25 +919,25 @@ const onboardingConfigWithLinks = [
key: 'vm',
label: 'VM',
imgUrl: vmUrl,
link: '/docs/instrumentation/javascript/opentelemetry-nextjs/',
link: '/docs/instrumentation/opentelemetry-nextjs/',
},
{
key: 'k8s',
label: 'Kubernetes',
imgUrl: kubernetesUrl,
link: '/docs/instrumentation/javascript/opentelemetry-nextjs/',
link: '/docs/instrumentation/opentelemetry-nextjs/',
},
{
key: 'docker',
label: 'Docker',
imgUrl: dockerUrl,
link: '/docs/instrumentation/javascript/opentelemetry-nextjs/',
link: '/docs/instrumentation/opentelemetry-nextjs/',
},
{
key: 'windows',
label: 'Windows',
imgUrl: windowsUrl,
link: '/docs/instrumentation/javascript/opentelemetry-nextjs/',
link: '/docs/instrumentation/opentelemetry-nextjs/',
},
],
},
@@ -946,7 +946,7 @@ const onboardingConfigWithLinks = [
key: 'nuxtjs',
label: 'NuxtJS',
imgUrl: nuxtjsUrl,
link: '/docs/instrumentation/javascript/opentelemetry-nuxtjs/',
link: '/docs/instrumentation/opentelemetry-nuxtjs/',
question: {
desc: 'What is your Environment?',
type: 'select',
@@ -956,25 +956,25 @@ const onboardingConfigWithLinks = [
key: 'vm',
label: 'VM',
imgUrl: vmUrl,
link: '/docs/instrumentation/javascript/opentelemetry-nuxtjs/',
link: '/docs/instrumentation/opentelemetry-nuxtjs/',
},
{
key: 'k8s',
label: 'Kubernetes',
imgUrl: kubernetesUrl,
link: '/docs/instrumentation/javascript/opentelemetry-nuxtjs/',
link: '/docs/instrumentation/opentelemetry-nuxtjs/',
},
{
key: 'docker',
label: 'Docker',
imgUrl: dockerUrl,
link: '/docs/instrumentation/javascript/opentelemetry-nuxtjs/',
link: '/docs/instrumentation/opentelemetry-nuxtjs/',
},
{
key: 'windows',
label: 'Windows',
imgUrl: windowsUrl,
link: '/docs/instrumentation/javascript/opentelemetry-nuxtjs/',
link: '/docs/instrumentation/opentelemetry-nuxtjs/',
},
],
},
@@ -983,7 +983,7 @@ const onboardingConfigWithLinks = [
key: 'react-native',
label: 'React Native',
imgUrl: reactjsUrl,
link: '/docs/instrumentation/javascript/opentelemetry-react-native/',
link: '/docs/instrumentation/opentelemetry-react-native/',
},
],
},
@@ -1298,7 +1298,7 @@ const onboardingConfigWithLinks = [
'resque',
],
id: 'ruby',
link: '/docs/instrumentation/opentelemetry-ruby/',
link: '/docs/instrumentation/opentelemetry-ruby-on-rails/',
question: {
desc: 'What is your Environment?',
type: 'select',
@@ -1308,25 +1308,25 @@ const onboardingConfigWithLinks = [
key: 'vm',
label: 'VM',
imgUrl: vmUrl,
link: '/docs/instrumentation/opentelemetry-ruby/',
link: '/docs/instrumentation/opentelemetry-ruby-on-rails/',
},
{
key: 'k8s',
label: 'Kubernetes',
imgUrl: kubernetesUrl,
link: '/docs/instrumentation/opentelemetry-ruby/',
link: '/docs/instrumentation/opentelemetry-ruby-on-rails/',
},
{
key: 'docker',
label: 'Docker',
imgUrl: dockerUrl,
link: '/docs/instrumentation/opentelemetry-ruby/',
link: '/docs/instrumentation/opentelemetry-ruby-on-rails/',
},
{
key: 'windows',
label: 'Windows',
imgUrl: windowsUrl,
link: '/docs/instrumentation/opentelemetry-ruby/',
link: '/docs/instrumentation/opentelemetry-ruby-on-rails/',
},
],
},
@@ -2292,7 +2292,7 @@ const onboardingConfigWithLinks = [
'python structured logging',
'python-logs-auto-instrumentation',
],
link: '/docs/logs-management/send-logs/python-logs/',
link: '/docs/userguide/python-logs-auto-instrumentation/',
},
{
dataSource: 'otel-java-sdk',
@@ -2323,7 +2323,7 @@ const onboardingConfigWithLinks = [
'Logback',
'log4j',
],
link: '/docs/logs-management/send-logs/java-logs/',
link: '/docs/userguide/collecting_application_logs_otel_sdk_java/',
},
{
dataSource: 'nodejs-logs',
@@ -2463,7 +2463,7 @@ const onboardingConfigWithLinks = [
'linux',
],
imgUrl: hostmetricsUrl,
link: '/docs/infrastructure-monitoring/user-guides/hostmetrics/',
link: '/docs/userguide/hostmetrics/',
},
{
dataSource: 'k8s-metrics',
@@ -2495,7 +2495,7 @@ const onboardingConfigWithLinks = [
'telemetry',
],
imgUrl: kubernetesUrl,
link: '/docs/infrastructure-monitoring/user-guides/k8s-metrics/',
link: '/docs/userguide/k8s-metrics/',
},
{
dataSource: 'docker-container-metrics',
@@ -2583,7 +2583,7 @@ const onboardingConfigWithLinks = [
key: 'aws-ec2-metrics-manual',
label: 'Manual Setup',
imgUrl: ec2Url,
link: '/docs/opentelemetry-collection-agents/vm/install/',
link: '/docs/tutorial/opentelemetry-binary-usage-in-virtual-machine/',
},
],
},
@@ -2772,7 +2772,7 @@ const onboardingConfigWithLinks = [
'vpc-logs',
],
imgUrl: vpcUrl,
link: '/docs/aws-monitoring/vpc/',
link: '/docs/aws-monitoring/vpc-logs/',
},
{
dataSource: 'aws-rds',
@@ -3837,7 +3837,7 @@ const onboardingConfigWithLinks = [
'traces',
'tracing',
],
link: '/docs/openai-monitoring/',
link: '/docs/llm/opentelemetry-openai-monitoring/',
},
{
dataSource: 'codex-monitoring',
@@ -4779,7 +4779,7 @@ const onboardingConfigWithLinks = [
link: '/docs/instrumentation/mobile-instrumentation/opentelemetry-flutter/',
},
{
dataSource: 'ios-swift',
dataSource: 'swift',
label: 'iOS Swift',
imgUrl: swiftMonitoringUrl,
tags: ['mobile app monitoring'],
@@ -4808,7 +4808,6 @@ const onboardingConfigWithLinks = [
'traces',
'tracing',
],
id: 'ios-swift',
link: '/docs/instrumentation/mobile-instrumentation/opentelemetry-swiftui/',
},
{
@@ -5474,7 +5473,7 @@ const onboardingConfigWithLinks = [
'telemetry',
],
id: 'hasura',
link: '/docs/integrations/outposts/hasura/',
link: '/docs/integrations/opentelemetry-hasura/',
},
{
dataSource: 'opencode-observability',
@@ -5936,7 +5935,7 @@ const onboardingConfigWithLinks = [
'metrics',
'monitoring',
],
link: '/docs/integrations/outposts/flyio/',
link: '/docs/integrations/flyio/',
},
{
dataSource: 'envoy',
@@ -6514,7 +6513,7 @@ const onboardingConfigWithLinks = [
'tracing',
],
id: 'openclaw-observability',
link: '/docs/openclaw-observability/',
link: '/docs/openclaw-monitoring/',
},
{
dataSource: 'claude-agent-monitoring',
@@ -6556,7 +6555,7 @@ const onboardingConfigWithLinks = [
'render monitoring',
],
id: 'render-metrics',
link: '/docs/integrations/outposts/render/',
link: '/docs/metrics-management/render-metrics/',
},
{
dataSource: 'cert-manager',
@@ -6580,7 +6579,7 @@ const onboardingConfigWithLinks = [
'tls',
],
id: 'cert-manager',
link: '/docs/infrastructure-monitoring/user-guides/cert-manager/',
link: '/docs/infrastructure-monitoring/cert-manager/',
},
{
dataSource: 'pgbouncer',

View File

@@ -1,4 +1,3 @@
import type { ExtendedSeries } from 'lib/uPlotV2/config/types';
import { Widgets } from 'types/api/widgets/widget';
import {
MetricRangePayloadProps,
@@ -165,27 +164,6 @@ describe('BarPanel utils', () => {
expect(config.series).toHaveLength(3);
});
it('passes partial timestamps to the series', () => {
const apiResponse = createApiResponse([
{
metric: {},
queryName: 'Q1',
values: [
[1000, '1'],
[1060, '0.2'],
],
partialTimestamps: [1060],
} as MetricRangePayloadProps['data']['result'][0],
]);
const series = prepareBarPanelConfig({
...baseParams,
apiResponse,
}).getConfig().series?.[1] as ExtendedSeries;
expect(series.partialTimestamps).toStrictEqual(new Set([1060]));
});
it('uses getLegend for label when currentQuery is provided', () => {
const apiResponse = createApiResponse([
{

View File

@@ -89,9 +89,6 @@ export function prepareBarPanelConfig({
isDarkMode,
stepInterval: currentStepInterval,
metric: series.metric,
partialTimestamps: series.partialTimestamps
? new Set(series.partialTimestamps)
: undefined,
});
});

View File

@@ -5,7 +5,6 @@ import {
} from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import type { ExtendedSeries } from 'lib/uPlotV2/config/types';
import { PanelMode } from 'lib/visualization/panels/types';
import { prepareUPlotConfig } from 'container/WidgetCard/Panels/TimeSeriesPanel/utils';
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
@@ -186,28 +185,6 @@ describe('TimeSeriesPanel utils', () => {
});
});
it('passes partial timestamps to the series', () => {
const apiResponse = createApiResponse([
{
metric: {},
queryName: 'Q1',
values: [
[1000, '1'],
[1060, '0.2'],
],
partialTimestamps: [1060],
} as MetricRangePayloadProps['data']['result'][0],
]);
const series = prepareUPlotConfig({
...baseParams,
apiResponse,
currentQuery: null as unknown as Query,
}).getConfig().series?.[1] as ExtendedSeries;
expect(series.partialTimestamps).toStrictEqual(new Set([1060]));
});
it('uses getLegend for label when currentQuery is provided', () => {
const apiResponse = createApiResponse([
{

View File

@@ -101,9 +101,6 @@ export const prepareUPlotConfig = ({
fillMode: widget.fillMode || FillMode.None,
isDarkMode,
metric: series.metric,
partialTimestamps: series.partialTimestamps
? new Set(series.partialTimestamps)
: undefined,
});
});

View File

@@ -1,42 +0,0 @@
import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange';
import { convertNewDataToOld } from '../convertNewDataToOld';
describe('convertNewDataToOld', () => {
it('carries partial values as epoch-second partialTimestamps', () => {
const payload = {
data: {
resultType: 'time_series',
result: [
{
queryName: 'A',
series: [
{
labels: {},
labelsArray: [],
values: [
{ timestamp: 1_000_000, value: '1' },
{ timestamp: 1_060_000, value: '0.2', partial: true },
],
},
{
labels: {},
labelsArray: [],
values: [{ timestamp: 1_000_000, value: '1' }],
},
],
},
],
},
} as unknown as MetricRangePayloadV3;
const [partial, complete] = convertNewDataToOld(payload).data.result;
expect(partial.values).toStrictEqual([
[1000, '1'],
[1060, '0.2'],
]);
expect(partial.partialTimestamps).toStrictEqual([1060]);
expect(complete).not.toHaveProperty('partialTimestamps');
});
});

View File

@@ -25,16 +25,11 @@ export const convertNewDataToOld = (
return [...acc, renderValues];
}, []);
const partialTimestamps = series.values
.filter((value) => value.partial)
.map((value) => value.timestamp / 1000);
const result: QueryData = {
metric: series.labels,
values,
queryName: `${item.queryName}`,
metaData: series.metaData,
...(partialTimestamps.length ? { partialTimestamps } : {}),
};
oldResult.push(result);

View File

@@ -122,27 +122,6 @@ describe('Tooltip', () => {
expect(screen.getByText(expectedTitle)).toBeInTheDocument();
});
it('marks the header partial when a visible series has a partial bucket at the cursor', () => {
const withSeries = (series: unknown[]): uPlot =>
Object.assign(createUPlotInstance(0), { series });
const partial = { show: true, partialTimestamps: new Set([1]) };
const { unmount } = renderTooltip({
uPlotInstance: withSeries([{}, partial]),
});
expect(screen.getByTestId('uplot-tooltip-header-partial')).toHaveTextContent(
'(partial)',
);
unmount();
renderTooltip({
uPlotInstance: withSeries([{}, { ...partial, show: false }]),
});
expect(
screen.queryByTestId('uplot-tooltip-header-partial'),
).not.toBeInTheDocument();
});
it('drops the date from the header title for a point on the current day', () => {
const todayTimestamp = dayjs().tz('UTC').startOf('hour').unix();
const uPlotInstance = createUPlotInstance(0, todayTimestamp);

View File

@@ -13,17 +13,6 @@
gap: var(--spacing-2);
}
.headerEnd {
display: flex;
align-items: center;
gap: var(--spacing-2);
}
.partialLabel {
font-style: italic;
opacity: 0.7;
}
.pinnedItem {
padding: var(--spacing-4);
}

View File

@@ -5,7 +5,6 @@ import { useTimezone } from 'providers/Timezone';
import type uPlot from 'uplot';
import { formatTimestampOmittingTodaysDate } from 'utils/timeUtils';
import type { ExtendedSeries } from '../../../../config/types';
import { TooltipContentItem } from '../../../types';
import TooltipItem from '../TooltipItem/TooltipItem';
import TooltipPinnedBadge from '../TooltipPinnedBadge/TooltipPinnedBadge';
@@ -35,7 +34,7 @@ export default function TooltipHeader({
const { timezone: userTimezone } = useTimezone();
const resolvedTimezone = timezone?.value ?? userTimezone.value;
const header = useMemo(() => {
const headerTitle = useMemo(() => {
if (!showTooltipHeader) {
return null;
}
@@ -47,21 +46,14 @@ export default function TooltipHeader({
if (timestamp == null) {
return null;
}
return {
title: formatTimestampOmittingTodaysDate(
timestamp * 1000,
resolvedTimezone,
dateFormat,
),
isPartial: (uPlotInstance.series ?? []).some(
(series: ExtendedSeries) =>
series.show && series.partialTimestamps?.has(timestamp),
),
};
return formatTimestampOmittingTodaysDate(
timestamp * 1000,
resolvedTimezone,
dateFormat,
);
}, [
resolvedTimezone,
uPlotInstance.data,
uPlotInstance.series,
uPlotInstance.cursor.idx,
showTooltipHeader,
dateFormat,
@@ -72,22 +64,10 @@ export default function TooltipHeader({
className={Styles.headerContainer}
data-testid="uplot-tooltip-header-container"
>
{showTooltipHeader && header && (
{showTooltipHeader && headerTitle && (
<div className={cx(Styles.headerRow, headerRowClassName)}>
<span>{header.title}</span>
{(header.isPartial || isPinned) && (
<span className={Styles.headerEnd}>
{header.isPartial && (
<span
className={Styles.partialLabel}
data-testid="uplot-tooltip-header-partial"
>
(partial)
</span>
)}
{isPinned && <TooltipPinnedBadge />}
</span>
)}
<span>{headerTitle}</span>
{isPinned && <TooltipPinnedBadge />}
</div>
)}

View File

@@ -1,6 +1,5 @@
import { SeriesVisibilityItem } from 'lib/visualization/panels/types';
import { getStoredSeriesVisibility } from 'lib/visualization/panels/utils/legendVisibilityUtils';
import { partialPointsDrawHook } from 'lib/uPlotV2/hooks/partialPointsDrawHook';
import { ThresholdsDrawHookOptions } from 'lib/uPlotV2/hooks/types';
import { thresholdsDrawHook } from 'lib/uPlotV2/hooks/useThresholdsDrawHook';
import { resolveSeriesVisibility } from 'lib/uPlotV2/utils/seriesVisibility';
@@ -82,8 +81,6 @@ export class UPlotConfigBuilder extends ConfigBuilder<
private thresholds: Record<string, ThresholdsDrawHookOptions> = {};
private hasPartialPointsHook = false;
private tzDate: ((timestamp: number) => Date) | undefined;
private id: string;
@@ -180,10 +177,6 @@ export class UPlotConfigBuilder extends ConfigBuilder<
*/
addSeries(props: SeriesProps): void {
this.series.push(new UPlotSeriesBuilder(props));
if (props.partialTimestamps?.size && !this.hasPartialPointsHook) {
this.hasPartialPointsHook = true;
this.addHook('draw', partialPointsDrawHook);
}
}
getSeriesSpanGapsOptions(): SeriesSpanGapsOption[] {

View File

@@ -4,7 +4,6 @@ import { calculateWidthBasedOnStepInterval } from 'lib/uPlotV2/utils';
import uPlot, { Series } from 'uplot';
import { resolveFillOpacity, toAlphaHex } from '../utils/fillOpacity';
import { reportBarRect } from '../utils/barRect';
import { generateGradientFill } from '../utils/generateGradientFill';
import { isolatedPointFilter } from '../utils/seriesPointsFilter';
import {
@@ -219,14 +218,7 @@ export class UPlotSeriesBuilder extends ConfigBuilder<
}
getConfig(): ExtendedSeries {
const {
scaleKey,
label,
spanGaps,
show = true,
metric,
partialTimestamps,
} = this.props;
const { scaleKey, label, spanGaps, show = true, metric } = this.props;
const resolvedLineColor = this.getLineColor();
@@ -254,7 +246,6 @@ export class UPlotSeriesBuilder extends ConfigBuilder<
...pathConfig,
points: Object.keys(pointsConfig).length > 0 ? pointsConfig : undefined,
metric,
...(partialTimestamps?.size ? { partialTimestamps } : {}),
};
}
}
@@ -367,7 +358,6 @@ function getBarPathBuilder({
builders[barsCfgKey] = barsPathBuilderFactory({
size: [barWidthFactor, effectiveBarMaxWidth],
align: barAlignment,
each: reportBarRect,
});
}
@@ -382,7 +372,6 @@ function getBarPathBuilder({
builders[barsCfgKey] = barsPathBuilderFactory({
size: [barWidthFactor, barMaxWidth],
align: barAlignment,
each: reportBarRect,
});
}

View File

@@ -650,20 +650,4 @@ describe('UPlotConfigBuilder stacking', () => {
expect(builder.getConfig().bands).toStrictEqual([{ series: [1, 3] }]);
});
it('registers the partial points draw hook once, only for series with partial timestamps', () => {
const props: SeriesProps = {
scaleKey: 'y',
label: 'Requests',
colorMapping: {},
drawStyle: DrawStyle.Line,
};
const builder = new UPlotConfigBuilder({ id: 'partial' });
builder.addSeries(props);
expect(builder.getConfig().hooks?.draw ?? []).toHaveLength(0);
builder.addSeries({ ...props, partialTimestamps: new Set([40]) });
builder.addSeries({ ...props, partialTimestamps: new Set([40]) });
expect(builder.getConfig().hooks?.draw).toHaveLength(1);
});
});

View File

@@ -1,7 +1,6 @@
import { themeColors } from 'constants/theme';
import uPlot from 'uplot';
import { reportBarRect } from '../../utils/barRect';
import { isolatedPointFilter } from '../../utils/seriesPointsFilter';
import type { SeriesProps } from '../types';
import { DrawStyle, FillMode, LineInterpolation, LineStyle } from '../types';
@@ -399,29 +398,4 @@ describe('UPlotSeriesBuilder', () => {
expect(builder.getConfig().fill).toBeUndefined();
});
it('exposes partial timestamps only when there are some', () => {
const partialTimestamps = new Set([40]);
expect(
new UPlotSeriesBuilder(createBaseProps({ partialTimestamps })).getConfig(),
).toHaveProperty('partialTimestamps', partialTimestamps);
expect(
new UPlotSeriesBuilder(
createBaseProps({ partialTimestamps: new Set() }),
).getConfig(),
).not.toHaveProperty('partialTimestamps');
});
it('reports bar rects from bar path builders', () => {
const config = new UPlotSeriesBuilder(
createBaseProps({ drawStyle: DrawStyle.Bar, barMaxWidth: 321 }),
).getConfig();
config.paths?.({} as uPlot, 1, 0, 1);
expect(uPlot.paths.bars).toHaveBeenCalledWith(
expect.objectContaining({ each: reportBarRect }),
);
});
});

View File

@@ -201,7 +201,6 @@ export enum FillMode {
export type ExtendedSeries = Series & {
metric?: { [key: string]: string };
partialTimestamps?: Set<number>;
};
export interface SeriesProps extends LineConfig, PointsConfig, BarConfig {
@@ -230,8 +229,6 @@ export interface SeriesProps extends LineConfig, PointsConfig, BarConfig {
isDarkMode?: boolean;
stepInterval?: number;
metric?: { [key: string]: string };
/** Epoch-second x values whose bucket doesn't cover a full step; marked with a dotted ring. */
partialTimestamps?: Set<number>;
}
export interface LegendItem {

View File

@@ -1,77 +0,0 @@
import uPlot from 'uplot';
import type { ExtendedSeries } from '../../config/types';
import { reportBarRect } from '../../utils/barRect';
import { partialPointsDrawHook } from '../partialPointsDrawHook';
function createPlot(
series: Partial<ExtendedSeries>,
values: (number | null)[] = [1, 2, 3],
): { u: uPlot; ctx: Record<string, jest.Mock> } {
const ctx = {
save: jest.fn(),
restore: jest.fn(),
beginPath: jest.fn(),
rect: jest.fn(),
clip: jest.fn(),
arc: jest.fn(),
fill: jest.fn(),
stroke: jest.fn(),
setLineDash: jest.fn(),
strokeRect: jest.fn(),
};
const xValues = [10, 20, 30];
const u = {
ctx,
bbox: { left: 0, top: 0, width: 100, height: 100 },
data: [xValues, values],
series: [{}, { show: true, stroke: 'red', points: { size: 4 }, ...series }],
valToIdx: (value: number): number => xValues.indexOf(value),
valToPos: (value: number): number => value,
} as unknown as uPlot;
return { u, ctx };
}
describe('partialPointsDrawHook', () => {
it('draws a dot and a dotted ring on each partial point', () => {
const { u, ctx } = createPlot({ partialTimestamps: new Set([30]) });
partialPointsDrawHook(u);
expect(ctx.arc).toHaveBeenCalledTimes(2);
expect(ctx.arc).toHaveBeenCalledWith(30, 3, 2, 0, 2 * Math.PI);
expect(ctx.arc).toHaveBeenCalledWith(30, 3, 5, 0, 2 * Math.PI);
expect(ctx.setLineDash).toHaveBeenCalledWith([2, 2]);
expect(ctx.restore).toHaveBeenCalled();
});
it('skips hidden series, null values and timestamps not in the data', () => {
const hidden = createPlot({ show: false, partialTimestamps: new Set([30]) });
const missing = createPlot({ partialTimestamps: new Set([30]) }, [
1,
2,
null,
]);
const unknown = createPlot({ partialTimestamps: new Set([40]) });
[hidden, missing, unknown].forEach(({ u, ctx }) => {
partialPointsDrawHook(u);
expect(ctx.arc).not.toHaveBeenCalled();
});
});
it('draws a dotted outline around a partial bar', () => {
const { u, ctx } = createPlot({
partialTimestamps: new Set([30]),
paths: (plot, seriesIdx, idx) => {
reportBarRect(plot, seriesIdx, idx, 10, 20, 8, 30);
return null;
},
});
partialPointsDrawHook(u);
expect(ctx.strokeRect).toHaveBeenCalledWith(7, 17, 14, 36);
expect(ctx.arc).not.toHaveBeenCalled();
});
});

View File

@@ -1,89 +0,0 @@
import uPlot from 'uplot';
import { ExtendedSeries } from '../config/types';
import { BarRect, measureBar } from '../utils/barRect';
const DEFAULT_POINT_SIZE = 5;
const RING_GAP = 3;
const RING_WIDTH = 1.5;
const RING_DASH = [2, 2];
function drawRing(
u: uPlot,
series: ExtendedSeries,
idx: number,
value: number,
): void {
const { ctx } = u;
const { pxRatio } = uPlot;
const cx = u.valToPos(u.data[0][idx], 'x', true);
const cy = u.valToPos(value, series.scale ?? 'y', true);
const radius = ((series.points?.size ?? DEFAULT_POINT_SIZE) / 2) * pxRatio;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, 2 * Math.PI);
ctx.fill();
ctx.lineWidth = RING_WIDTH * pxRatio;
ctx.setLineDash(RING_DASH.map((segment) => segment * pxRatio));
ctx.beginPath();
ctx.arc(cx, cy, radius + RING_GAP * pxRatio, 0, 2 * Math.PI);
ctx.stroke();
}
function drawBarOutline(u: uPlot, rect: BarRect): void {
const gap = RING_GAP * uPlot.pxRatio;
u.ctx.lineWidth = RING_WIDTH * uPlot.pxRatio;
u.ctx.setLineDash(RING_DASH.map((segment) => segment * uPlot.pxRatio));
u.ctx.strokeRect(
rect.left - gap,
rect.top - gap,
rect.width + 2 * gap,
rect.height + 2 * gap,
);
}
/**
* Marks each series' partial buckets: a dotted outline around bars, a dot and dotted
* ring on line and point series.
*/
export function partialPointsDrawHook(u: uPlot): void {
const { ctx, bbox } = u;
const xValues = u.data[0];
ctx.save();
ctx.beginPath();
ctx.rect(bbox.left, bbox.top, bbox.width, bbox.height);
ctx.clip();
for (let seriesIdx = 1; seriesIdx < u.series.length; seriesIdx += 1) {
const series = u.series[seriesIdx] as ExtendedSeries;
if (!series.show || !series.partialTimestamps) {
continue;
}
const color =
typeof series.stroke === 'function'
? series.stroke(u, seriesIdx)
: (series.stroke ?? '');
ctx.fillStyle = color;
ctx.strokeStyle = color;
ctx.globalAlpha = series.alpha ?? 1;
series.partialTimestamps.forEach((timestamp) => {
const idx = u.valToIdx(timestamp);
const value = u.data[seriesIdx][idx];
if (xValues[idx] !== timestamp || value == null) {
return;
}
ctx.setLineDash([]);
const bar = measureBar(u, seriesIdx, idx);
if (bar) {
drawBarOutline(u, bar);
} else {
drawRing(u, series, idx, value);
}
});
}
ctx.restore();
}

View File

@@ -1,30 +0,0 @@
import type uPlot from 'uplot';
import { measureBar, reportBarRect } from '../barRect';
function plotWithPaths(paths?: uPlot.Series['paths']): uPlot {
return { series: [{}, { paths }] } as unknown as uPlot;
}
describe('measureBar', () => {
it('returns the rect the bar paths report for that index', () => {
const paths = jest.fn((u: uPlot, seriesIdx: number, idx: number) => {
reportBarRect(u, seriesIdx, idx, 10, 20, 8, 30);
return null;
});
const u = plotWithPaths(paths);
expect(measureBar(u, 1, 4)).toStrictEqual({
left: 10,
top: 20,
width: 8,
height: 30,
});
expect(paths).toHaveBeenCalledWith(u, 1, 4, 4);
});
it('returns null for series whose paths report no bar', () => {
expect(measureBar(plotWithPaths(jest.fn(() => null)), 1, 0)).toBeNull();
expect(measureBar(plotWithPaths(), 1, 0)).toBeNull();
});
});

View File

@@ -1,45 +0,0 @@
import type uPlot from 'uplot';
export interface BarRect {
left: number;
top: number;
width: number;
height: number;
}
let onBarRect: ((rect: BarRect) => void) | null = null;
/** `each` callback for bar path builders; reports bar rects while {@link measureBar} runs. */
// eslint-disable-next-line max-params
export function reportBarRect(
_u: uPlot,
_seriesIdx: number,
_idx: number,
left: number,
top: number,
width: number,
height: number,
): void {
onBarRect?.({ left, top, width, height });
}
/**
* Canvas rect of one bar, from re-running the series' paths for that index.
* `null` for series not drawn as bars.
*/
export function measureBar(
u: uPlot,
seriesIdx: number,
idx: number,
): BarRect | null {
let rect: BarRect | null = null;
onBarRect = (measured): void => {
rect = measured;
};
try {
u.series[seriesIdx].paths?.(u, seriesIdx, idx, idx);
} finally {
onBarRect = null;
}
return rect;
}

View File

@@ -15,7 +15,6 @@ import {
import { resolveSeriesLabelV5 } from 'pages/DashboardPage/DashboardContainer/Panels/utils/resolveSeriesLabel';
import type { PanelSeries } from 'pages/DashboardPage/DashboardContainer/queryV5/types';
import {
getPartialTimestamps,
hasSingleVisiblePoint,
toClickPluginPayload,
} from 'pages/DashboardPage/DashboardContainer/queryV5/uplotData';
@@ -156,7 +155,6 @@ function addSeries({
fillOpacity,
isDarkMode,
metric: s.labels,
partialTimestamps: getPartialTimestamps(s.values),
});
});
}

View File

@@ -1,34 +0,0 @@
import type { DashboardtypesBarChartPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { UTC_TIMEZONE } from 'components/CustomTimePicker/timezoneUtils';
import type { ExtendedSeries } from 'lib/uPlotV2/config/types';
import { PanelMode } from 'lib/visualization/panels/types';
import { buildBarChartConfig } from '../buildConfig';
describe('buildBarChartConfig partial buckets', () => {
it('passes partial timestamps (epoch seconds) to the series', () => {
const config = buildBarChartConfig({
panelId: 'p1',
spec: {} as DashboardtypesBarChartPanelSpecDTO,
builderQueries: [],
series: [
{
queryName: 'A',
legend: 'A',
labels: {},
values: [
{ timestamp: 1000, value: 1 },
{ timestamp: 61000, value: 0.2, partial: true },
],
kind: 'series',
aggregation: { index: 0, alias: 'A' },
},
],
isDarkMode: false,
timezone: UTC_TIMEZONE,
panelMode: PanelMode.DASHBOARD_VIEW,
}).getConfig().series?.[1] as ExtendedSeries;
expect(config.partialTimestamps).toStrictEqual(new Set([61]));
});
});

View File

@@ -5,10 +5,7 @@ import {
} from 'pages/DashboardPage/DashboardContainer/Panels/utils/baseConfigBuilder';
import { resolveSeriesLabelV5 } from 'pages/DashboardPage/DashboardContainer/Panels/utils/resolveSeriesLabel';
import type { PanelSeries } from 'pages/DashboardPage/DashboardContainer/queryV5/types';
import {
getPartialTimestamps,
toClickPluginPayload,
} from 'pages/DashboardPage/DashboardContainer/queryV5/uplotData';
import { toClickPluginPayload } from 'pages/DashboardPage/DashboardContainer/queryV5/uplotData';
import getLabelName from 'lib/getLabelName';
import { DrawStyle } from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
@@ -105,7 +102,6 @@ function addSeries({
isDarkMode,
stepInterval,
metric: s.labels,
partialTimestamps: getPartialTimestamps(s.values),
});
});
}

View File

@@ -1,6 +1,5 @@
import type { DashboardtypesTimeSeriesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { UTC_TIMEZONE } from 'components/CustomTimePicker/timezoneUtils';
import type { ExtendedSeries } from 'lib/uPlotV2/config/types';
import { PanelMode } from 'lib/visualization/panels/types';
import type { PanelSeries } from 'pages/DashboardPage/DashboardContainer/queryV5/types';
@@ -75,27 +74,3 @@ describe('buildTimeSeriesConfig spanGaps', () => {
).toStrictEqual([10]);
});
});
describe('buildTimeSeriesConfig partial buckets', () => {
it('passes partial timestamps (epoch seconds) to the series', () => {
const config = buildTimeSeriesConfig({
panelId: 'p1',
spec: {} as DashboardtypesTimeSeriesPanelSpecDTO,
builderQueries: [],
series: [
{
...series[0],
values: [
{ timestamp: 1000, value: 1 },
{ timestamp: 61000, value: 0.2, partial: true },
],
},
],
isDarkMode: false,
timezone: UTC_TIMEZONE,
panelMode: PanelMode.DASHBOARD_VIEW,
}).getConfig().series?.[1] as ExtendedSeries;
expect(config.partialTimestamps).toStrictEqual(new Set([61]));
});
});

View File

@@ -13,7 +13,6 @@ import { resolveSpanGaps } from 'pages/DashboardPage/DashboardContainer/Panels/u
import { resolveSeriesLabelV5 } from 'pages/DashboardPage/DashboardContainer/Panels/utils/resolveSeriesLabel';
import type { PanelSeries } from 'pages/DashboardPage/DashboardContainer/queryV5/types';
import {
getPartialTimestamps,
hasSingleVisiblePoint,
toClickPluginPayload,
} from 'pages/DashboardPage/DashboardContainer/queryV5/uplotData';
@@ -150,7 +149,6 @@ function addSeries({
fillMode,
isDarkMode,
metric: s.labels,
partialTimestamps: getPartialTimestamps(s.values),
});
});
}

View File

@@ -1,6 +1,5 @@
import type { PanelSeries } from '../types';
import {
getPartialTimestamps,
hasSingleVisiblePoint,
prepareAlignedData,
toClickPluginPayload,
@@ -86,22 +85,6 @@ describe('hasSingleVisiblePoint', () => {
});
});
describe('getPartialTimestamps', () => {
it('returns partial timestamps in epoch seconds', () => {
expect(
getPartialTimestamps([
{ timestamp: 1_000_500, value: 1, partial: true },
{ timestamp: 2_000_000, value: 2 },
{ timestamp: 3_000_000, value: 3, partial: true },
]),
).toStrictEqual(new Set([1000, 3000]));
});
it('returns undefined when nothing is partial', () => {
expect(getPartialTimestamps([{ timestamp: 1000, value: 1 }])).toBeUndefined();
});
});
describe('toClickPluginPayload', () => {
it('produces the tuple-shaped legacy result the shared click plugin reads', () => {
const payload = toClickPluginPayload([

View File

@@ -41,19 +41,6 @@ export function prepareAlignedData(series: PanelSeries[]): uPlot.AlignedData {
return [timestamps, ...yValues] as uPlot.AlignedData;
}
/** Epoch-second x values of the series' partial buckets; `undefined` when none are partial. */
export function getPartialTimestamps(
values: PanelSeriesPoint[],
): Set<number> | undefined {
const timestamps = new Set<number>();
values.forEach((point) => {
if (point.partial) {
timestamps.add(Math.floor(point.timestamp / 1000));
}
});
return timestamps.size ? timestamps : undefined;
}
/**
* True when the series has at most one finite point — can't draw a line, so renderers degrade to
* points (V1 `hasSingleVisiblePoint` parity).

View File

@@ -1,11 +1,11 @@
import { ReactNode } from 'react';
import { Tooltip, TooltipProps } from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import styles from './EntityMetadataItem.module.scss';
interface EntityMetadataItemProps {
tooltip: TooltipProps['title'];
tooltip: string;
icon?: ReactNode;
children: ReactNode;
}

View File

@@ -8,12 +8,3 @@
// Keep Typography.Text at the row's size (it reads this var, not inherited font-size).
--typography-font-size: var(--periscope-font-size-base);
}
.tokenTooltip {
composes: tooltipBody from '../shared.module.scss';
min-width: 180px;
}
.tokenTooltipTitle {
padding-bottom: var(--spacing-4);
}

View File

@@ -1,18 +1,10 @@
import { CalendarClock, Coins, Landmark, Server, Timer } from '@signozhq/icons';
import { CalendarClock, Server, Timer } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import cx from 'classnames';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import HttpStatusBadge from 'components/HttpStatusBadge/HttpStatusBadge';
import {
AiTokenCounts,
formatCost,
formatTokens,
getTotalInputTokens,
hasValue,
} from '../utils/aiTraceUtils';
import EntityMetadataItem from './EntityMetadataItem';
import TokenUsageTooltip from './TokenUsageTooltip';
import styles from './EntityMetadataRow.module.scss';
@@ -24,8 +16,6 @@ interface EntityMetadataRowProps {
execTimePercent?: number;
timestamp?: string;
statusCode?: string | number;
tokens?: AiTokenCounts;
cost?: number | null;
}
const ICON_SIZE = 14;
@@ -42,11 +32,8 @@ function EntityMetadataRow({
execTimePercent,
timestamp,
statusCode,
tokens,
cost,
}: EntityMetadataRowProps): JSX.Element {
const entityLabel = entity === 'trace' ? 'Trace' : 'Span';
const totalInput = tokens && getTotalInputTokens(tokens);
const durationTooltip =
entity === 'trace' ? 'Trace Duration' : 'Span Duration';
// Single source of duration formatting so both rows label units identically.
@@ -104,24 +91,6 @@ function EntityMetadataRow({
<HttpStatusBadge statusCode={statusCode} />
</EntityMetadataItem>
)}
{tokens && (
<EntityMetadataItem
tooltip={<TokenUsageTooltip tokens={tokens} />}
icon={<Coins size={ICON_SIZE} />}
>
Tokens: {formatTokens(totalInput)} → {formatTokens(tokens.output)}
</EntityMetadataItem>
)}
{hasValue(cost) && (
<EntityMetadataItem
tooltip="Total cost"
icon={<Landmark size={ICON_SIZE} />}
>
Cost — {formatCost(cost)}
</EntityMetadataItem>
)}
</div>
);
}
@@ -133,8 +102,6 @@ EntityMetadataRow.defaultProps = {
execTimePercent: undefined,
timestamp: undefined,
statusCode: undefined,
tokens: undefined,
cost: undefined,
};
export default EntityMetadataRow;

View File

@@ -1,35 +0,0 @@
import { Typography } from '@signozhq/ui/typography';
import TokenBreakdown from '../TokenBreakdown/TokenBreakdown';
import { AiTokenCounts } from '../utils/aiTraceUtils';
import styles from './EntityMetadataRow.module.scss';
interface TokenUsageTooltipProps {
tokens: AiTokenCounts;
}
function TokenUsageTooltip({ tokens }: TokenUsageTooltipProps): JSX.Element {
const { input, output, cacheRead, cacheWrite, reasoning } = tokens;
return (
<div className={styles.tokenTooltip}>
<Typography.Text
size="small"
weight="normal"
className={styles.tokenTooltipTitle}
>
Usage Breakdown
</Typography.Text>
<TokenBreakdown
input={input}
output={output}
cacheRead={cacheRead}
cacheWrite={cacheWrite}
reasoning={reasoning}
/>
</div>
);
}
export default TokenUsageTooltip;

View File

@@ -1,11 +1,9 @@
import { useMemo } from 'react';
import { Link2 } from '@signozhq/icons';
import dayjs from 'dayjs';
import KeyValueLabel from 'periscope/components/KeyValueLabel';
import { SpanV3 } from 'types/api/trace/getTraceV3';
import EntityMetadataRow from '../EntityMetadata/EntityMetadataRow';
import { getAIUsageDetails, convertToAiTokens } from '../utils/aiTraceUtils';
import { HIGHLIGHTED_OPTIONS } from './config';
import {
LinkedSpansPanel,
@@ -33,10 +31,6 @@ function SpanSummary({
}: SpanSummaryProps): JSX.Element {
const percentile = useSpanPercentile(selectedSpan);
const linkedSpans = useLinkedSpans((selectedSpan as any).references);
const aiUsage = useMemo(
() => getAIUsageDetails(selectedSpan)?.usage,
[selectedSpan],
);
return (
<>
@@ -69,8 +63,6 @@ function SpanSummary({
: undefined
}
timestamp={dayjs(selectedSpan.timestamp).format('HH:mm:ss — MMM D, YYYY')}
tokens={aiUsage && convertToAiTokens(aiUsage)}
cost={aiUsage?.cost}
/>
<div className={styles.spanInfoItem}>

View File

@@ -95,26 +95,4 @@ describe('SpanSummary', () => {
expect(screen.queryByText(/of total exec time/)).not.toBeInTheDocument();
});
it('renders tokens and cost from the gen_ai usage attributes', () => {
const span = {
...createMockSpan(),
attributes: {
'gen_ai.usage.input_tokens': 1000,
'gen_ai.usage.output_tokens': 9,
'signoz.gen_ai.usage.tokens.cost': 0.000009,
},
};
render(<SpanSummary selectedSpan={span} />);
expect(screen.getByText(/Tokens: 1,000 → 9/)).toBeInTheDocument();
expect(screen.getByText(/Cost — \$ 0.000009/)).toBeInTheDocument();
});
it('omits tokens and cost for non-AI spans', () => {
render(<SpanSummary selectedSpan={createMockSpan()} />);
expect(screen.queryByText(/Tokens:/)).not.toBeInTheDocument();
expect(screen.queryByText(/Cost —/)).not.toBeInTheDocument();
});
});

View File

@@ -1,37 +0,0 @@
import { Badge } from '@signozhq/ui/badge';
import HttpStatusBadge from 'components/HttpStatusBadge/HttpStatusBadge';
import { SpanV3 } from 'types/api/trace/getTraceV3';
import { getAiSpanKind } from '../utils/aiTraceUtils';
import { AI_SPAN_BADGES } from './constants';
interface SpanHintBadgeProps {
span: SpanV3;
}
function SpanHintBadge({ span }: SpanHintBadgeProps): JSX.Element | null {
const aiSpanKind = getAiSpanKind(span);
if (aiSpanKind) {
const { label, color } = AI_SPAN_BADGES[aiSpanKind];
return (
<span style={{ display: 'inline-flex' }}>
<Badge
variant="outlined"
color={color}
testId={`span-hint-badge-${aiSpanKind}`}
>
{label}
</Badge>
</span>
);
}
if (!span.response_status_code) {
return null;
}
return <HttpStatusBadge statusCode={span.response_status_code} />;
}
export default SpanHintBadge;

View File

@@ -1,12 +0,0 @@
import type { BadgeColorType } from '@signozhq/ui/badge';
import { AiSpanKind } from '../utils/aiTraceUtils';
export const AI_SPAN_BADGES: Record<
AiSpanKind,
{ label: string; color: BadgeColorType }
> = {
[AiSpanKind.Llm]: { label: 'LLM', color: 'success' },
[AiSpanKind.Tool]: { label: 'TOOL', color: 'warning' },
[AiSpanKind.Agent]: { label: 'AGENT', color: 'highlight-danger' },
};

View File

@@ -8,12 +8,16 @@
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--l2-background);
border: 1px solid var(--l2-border);
border-radius: 3px;
padding: 2px 6px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--l2-foreground);
margin-bottom: 6px;
}
.name {

View File

@@ -9,17 +9,28 @@
}
.content {
composes: tooltipBody from '../shared.module.scss';
min-width: 223px;
max-width: 360px;
font-size: 12px;
color: var(--l1-foreground);
}
.title {
composes: tooltipTitle from '../shared.module.scss';
.name {
font-weight: 600;
margin-bottom: 4px;
}
.header {
display: flex;
flex-direction: column;
padding-bottom: var(--spacing-2);
.row {
line-height: 1.5;
color: var(--l2-foreground);
}
.preview {
// container for additional preview rows
}
.previewKey {
color: var(--l2-foreground);
}
.previewValue {
color: var(--l1-foreground);
}

View File

@@ -1,5 +1,4 @@
import { Tooltip } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import { convertTimeToRelevantUnit } from 'utils/traceUtils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useTraceStore } from 'pages/TraceDetailsV3/stores/traceStore';
@@ -8,11 +7,6 @@ import { useMemo } from 'react';
import { SpanV3 } from 'types/api/trace/getTraceV3';
import { toFixed } from 'utils/toFixed';
import { getAIUsageDetails, SpanAiDetails } from '../utils/aiTraceUtils';
import SpanUsageBreakdown from './SpanUsageBreakdown';
import TraceTooltipRow from '../TraceTooltip/TraceTooltipRow';
import TraceTooltipSection from '../TraceTooltip/TraceTooltipSection';
import styles from './SpanHoverCard.module.scss';
/**
@@ -39,7 +33,6 @@ export interface SpanTooltipContentProps {
relativeStartMs: number;
durationMs: number;
previewRows?: SpanPreviewRow[];
ai?: SpanAiDetails;
}
export function SpanTooltipContent({
@@ -49,60 +42,33 @@ export function SpanTooltipContent({
relativeStartMs,
durationMs,
previewRows,
ai,
}: SpanTooltipContentProps): JSX.Element {
const { time: formattedDuration, timeUnitName } =
convertTimeToRelevantUnit(durationMs);
return (
<div className={styles.content}>
<div className={styles.header}>
<Typography.Text
className={styles.title}
size="small"
weight="normal"
style={{ color }}
>
{spanName}
</Typography.Text>
{ai?.usage && (
<Typography.Text size="small" weight="normal">
Usage Breakdown
</Typography.Text>
)}
<div className={styles.name} style={{ color }}>
{spanName}
</div>
<div className={styles.row}>status: {hasError ? 'error' : 'ok'}</div>
<div className={styles.row}>start: {toFixed(relativeStartMs, 2)} ms</div>
<div className={styles.row}>
duration: {toFixed(formattedDuration, 2)} {timeUnitName}
</div>
{ai?.usage && <SpanUsageBreakdown usage={ai.usage} />}
<TraceTooltipSection>
{ai?.model && <TraceTooltipRow label="model" value={ai.model} />}
{ai?.toolName && <TraceTooltipRow label="tool" value={ai.toolName} />}
{ai?.agentName && <TraceTooltipRow label="agent" value={ai.agentName} />}
<TraceTooltipRow
label="status"
value={hasError ? 'error' : 'ok'}
testId="span-hover-card-status"
/>
<TraceTooltipRow
label="start"
value={`${toFixed(relativeStartMs, 2)} ms`}
testId="span-hover-card-start"
/>
<TraceTooltipRow
label="duration"
value={`${toFixed(formattedDuration, 2)} ${timeUnitName}`}
testId="span-hover-card-duration"
/>
</TraceTooltipSection>
{previewRows && previewRows.length > 0 && (
<TraceTooltipSection>
<div className={styles.preview}>
{previewRows.map((row) => (
<TraceTooltipRow
<div
key={row.key}
label={row.key}
value={row.value}
testId={`span-hover-card-preview-${row.key}`}
/>
className={styles.row}
data-testid={`span-hover-card-preview-${row.key}`}
>
<span className={styles.previewKey}>{row.key}:</span>{' '}
<span className={styles.previewValue}>{row.value}</span>
</div>
))}
</TraceTooltipSection>
</div>
)}
</div>
);
@@ -164,7 +130,6 @@ export function SpanHoverCard({
relativeStartMs: span.timestamp - traceStartTime,
durationMs: span.duration_nano / 1e6,
previewRows,
ai: getAIUsageDetails(span),
},
};
}, [

View File

@@ -1,38 +0,0 @@
import { formatCost, hasValue, SpanAiUsage } from '../utils/aiTraceUtils';
import TokenBreakdown from '../TokenBreakdown/TokenBreakdown';
import TraceTooltipRow from '../TraceTooltip/TraceTooltipRow';
import TraceTooltipSection from '../TraceTooltip/TraceTooltipSection';
interface SpanUsageBreakdownProps {
usage: SpanAiUsage;
}
function SpanUsageBreakdown({ usage }: SpanUsageBreakdownProps): JSX.Element {
const {
inputTokens,
outputTokens,
cacheReadTokens,
cacheCreationTokens,
reasoningTokens,
cost,
} = usage;
return (
<>
<TokenBreakdown
input={inputTokens}
output={outputTokens}
cacheRead={cacheReadTokens}
cacheWrite={cacheCreationTokens}
reasoning={reasoningTokens}
/>
{hasValue(cost) && (
<TraceTooltipSection>
<TraceTooltipRow isHeading label="Cost" value={formatCost(cost)} />
</TraceTooltipSection>
)}
</>
);
}
export default SpanUsageBreakdown;

View File

@@ -1,81 +0,0 @@
import {
AiTokenCounts,
formatTokens,
getTokenLabel,
getTotalInputTokens,
} from '../utils/aiTraceUtils';
import TraceTooltipRow from '../TraceTooltip/TraceTooltipRow';
import TraceTooltipSection from '../TraceTooltip/TraceTooltipSection';
type TokenBreakdownProps = AiTokenCounts;
function TokenBreakdown({
input,
output,
cacheRead,
cacheWrite,
reasoning,
}: TokenBreakdownProps): JSX.Element {
const totalInput = getTotalInputTokens({ input, cacheRead, cacheWrite });
return (
<>
<TraceTooltipSection>
<TraceTooltipRow
label={getTokenLabel('Total Input', totalInput)}
value={formatTokens(totalInput)}
isHeading
/>
<TraceTooltipRow
label={getTokenLabel('Cache Read', cacheRead)}
value={formatTokens(cacheRead)}
isNested
/>
<TraceTooltipRow
label={getTokenLabel('Cache Write', cacheWrite)}
value={formatTokens(cacheWrite)}
isNested
/>
<TraceTooltipRow
label={getTokenLabel('Input', input)}
value={formatTokens(input)}
isNested
/>
</TraceTooltipSection>
<TraceTooltipSection>
<TraceTooltipRow
label={getTokenLabel('Reasoning', reasoning)}
value={formatTokens(reasoning)}
isHeading
/>
</TraceTooltipSection>
<TraceTooltipSection>
<TraceTooltipRow
label={getTokenLabel('Output', output)}
value={formatTokens(output)}
isHeading
/>
</TraceTooltipSection>
<TraceTooltipSection>
<TraceTooltipRow
label="Total Token Usage"
value={formatTokens(totalInput + (output ?? 0))}
isHeading
/>
</TraceTooltipSection>
</>
);
}
TokenBreakdown.defaultProps = {
input: undefined,
output: undefined,
cacheRead: undefined,
cacheWrite: undefined,
reasoning: undefined,
};
export default TokenBreakdown;

View File

@@ -186,8 +186,6 @@ function TraceDetailsHeader({
DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS,
)}
statusCode={traceSummary.rootSpanStatusCode}
tokens={traceSummary.ai?.tokens}
cost={traceSummary.ai?.totalCost}
/>
) : (
<DetailsLoader />

View File

@@ -1,7 +1,6 @@
import { fireEvent, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import type { SpantypesGettableTraceSummaryDTO } from 'api/generated/services/sigNoz.schemas';
import ROUTES from 'constants/routes';
import { render } from 'tests/test-utils';
@@ -177,19 +176,9 @@ describe('TraceDetailsHeader – trace metadata row', () => {
hasMissingSpans: false,
totalSpansCount: 42,
totalErrorSpansCount: 0,
ai: {
tokens: {
input: 12040,
output: 3110,
cacheRead: 0,
cacheWrite: 0,
reasoning: 0,
},
totalCost: 0.0421,
},
};
const mockSummary = (data?: SpantypesGettableTraceSummaryDTO): void => {
const mockSummary = (data?: typeof traceMetadata): void => {
jest.mocked(useTraceSummary).mockReturnValue({ data, isLoading: false });
};
@@ -212,77 +201,6 @@ describe('TraceDetailsHeader – trace metadata row', () => {
expect(screen.getByText(duration)).toBeInTheDocument();
});
it('renders AI tokens and cost when the summary has them', () => {
mockSummary(traceMetadata);
render(<TraceDetailsHeader {...baseProps} showTraceDetailsHeaderOptions />);
expect(screen.getByText('Tokens: 12,040 → 3,110')).toBeInTheDocument();
expect(screen.getByText('Cost — $ 0.0421')).toBeInTheDocument();
});
it('omits AI tokens and cost when the summary has no ai field', () => {
mockSummary({ ...traceMetadata, ai: undefined });
render(<TraceDetailsHeader {...baseProps} showTraceDetailsHeaderOptions />);
expect(screen.queryByText(/^Tokens:/)).not.toBeInTheDocument();
expect(screen.queryByText(/^Cost —/)).not.toBeInTheDocument();
});
it('shows AI tokens when input and output are both zero', () => {
mockSummary({
...traceMetadata,
ai: {
...traceMetadata.ai,
tokens: { ...traceMetadata.ai.tokens, input: 0, output: 0 },
},
} as SpantypesGettableTraceSummaryDTO);
render(<TraceDetailsHeader {...baseProps} showTraceDetailsHeaderOptions />);
expect(screen.getByText('Tokens: 0 → 0')).toBeInTheDocument();
expect(screen.getByText('Cost — $ 0.0421')).toBeInTheDocument();
});
it('shows zero for a missing input', () => {
mockSummary({
...traceMetadata,
ai: {
...traceMetadata.ai,
tokens: { ...traceMetadata.ai.tokens, input: undefined },
},
} as unknown as SpantypesGettableTraceSummaryDTO);
render(<TraceDetailsHeader {...baseProps} showTraceDetailsHeaderOptions />);
expect(screen.getByText('Tokens: 0 → 3,110')).toBeInTheDocument();
});
it('shows zero AI tokens when input and output are both missing', () => {
mockSummary({
...traceMetadata,
ai: {
...traceMetadata.ai,
tokens: {
...traceMetadata.ai.tokens,
input: undefined,
output: undefined,
},
},
} as unknown as SpantypesGettableTraceSummaryDTO);
render(<TraceDetailsHeader {...baseProps} showTraceDetailsHeaderOptions />);
expect(screen.getByText('Tokens: 0 → 0')).toBeInTheDocument();
});
it('omits the cost when the summary sends null', () => {
mockSummary({
...traceMetadata,
ai: { ...traceMetadata.ai, totalCost: null },
} as unknown as SpantypesGettableTraceSummaryDTO);
render(<TraceDetailsHeader {...baseProps} showTraceDetailsHeaderOptions />);
expect(screen.getByText('Tokens: 12,040 → 3,110')).toBeInTheDocument();
expect(screen.queryByText(/^Cost —/)).not.toBeInTheDocument();
});
it('is shown by default and can be hidden / shown again via the Trace options menu', async () => {
const user = userEvent.setup({ delay: null });
mockSummary(traceMetadata);

View File

@@ -30,9 +30,13 @@
// lives here. Shares visual treatment with the waterfall hover popover
// but is positioned `fixed` instead of anchored.
.tooltip {
composes: tooltipSurface from '../shared.module.scss';
position: fixed;
z-index: 1000;
pointer-events: none;
background-color: var(--l1-background);
padding: 8px 12px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border: 1px solid var(--l2-border);
color: var(--l1-foreground);
}

View File

@@ -8,7 +8,6 @@ import { EventTooltipContent } from '../SpanHoverCard/EventTooltipContent';
import { SpanTooltipContent } from '../SpanHoverCard/SpanHoverCard';
import { DEFAULT_ROW_HEIGHT } from './constants';
import { useCanvasSetup } from './hooks/useCanvasSetup';
import { useCursorTooltipPosition } from './hooks/useCursorTooltipPosition';
import { useFlamegraphCrosshair } from './hooks/useFlamegraphCrosshair';
import { useFlamegraphDrag } from './hooks/useFlamegraphDrag';
import { useFlamegraphDraw } from './hooks/useFlamegraphDraw';
@@ -201,11 +200,15 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
handleHoverMouseLeave,
]);
const tooltipRef = useCursorTooltipPosition<HTMLDivElement>(tooltipContent);
const tooltipElement = tooltipContent
? createPortal(
<div ref={tooltipRef} className={styles.tooltip}>
<div
className={styles.tooltip}
style={{
left: Math.min(tooltipContent.clientX + 15, window.innerWidth - 220),
top: Math.min(tooltipContent.clientY + 15, window.innerHeight - 100),
}}
>
{tooltipContent.event ? (
<EventTooltipContent
eventName={tooltipContent.event.name}

View File

@@ -1,36 +0,0 @@
import { RefObject, useLayoutEffect, useRef } from 'react';
const CURSOR_OFFSET = 15;
interface CursorAnchor {
clientX: number;
clientY: number;
}
function resolveAxis(cursor: number, size: number, viewport: number): number {
const after = cursor + CURSOR_OFFSET;
if (after + size <= viewport) {
return after;
}
return Math.max(0, cursor - CURSOR_OFFSET - size);
}
/** Places a fixed element beside the cursor, flipping sides when it would overflow the viewport. */
export function useCursorTooltipPosition<T extends HTMLElement>(
anchor: CursorAnchor | null,
): RefObject<T> {
const ref = useRef<T>(null);
useLayoutEffect(() => {
const el = ref.current;
if (!el || !anchor) {
return;
}
const left = resolveAxis(anchor.clientX, el.offsetWidth, window.innerWidth);
const top = resolveAxis(anchor.clientY, el.offsetHeight, window.innerHeight);
el.style.left = `${left}px`;
el.style.top = `${top}px`;
}, [anchor]);
return ref;
}

View File

@@ -1,47 +0,0 @@
.section {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
padding: var(--spacing-2) 0;
border-top: 1px solid;
border-image: linear-gradient(
90deg,
var(--l3-background-transparent),
var(--l3-border),
var(--l3-background-transparent)
)
1;
}
.section:first-child {
padding-top: 0;
border-top: none;
}
.section:last-child {
padding-bottom: 0;
}
.row {
display: grid;
grid-template-columns: auto auto;
justify-content: space-between;
column-gap: var(--spacing-2);
min-width: 0;
}
.nestedRow {
padding-left: var(--spacing-6);
}
.rowLabel {
min-width: 0;
overflow-wrap: anywhere;
}
.rowValue {
min-width: 0;
overflow-wrap: anywhere;
text-align: right;
font-variant-numeric: tabular-nums;
}

View File

@@ -1,42 +0,0 @@
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import styles from './TraceTooltip.module.scss';
interface TraceTooltipRowProps {
label: string;
value?: string;
isNested?: boolean;
isHeading?: boolean;
testId?: string;
}
function TraceTooltipRow({
label,
value,
isNested,
isHeading,
testId,
}: TraceTooltipRowProps): JSX.Element {
return (
<div
className={cx(styles.row, isNested && styles.nestedRow)}
data-testid={testId}
>
<Typography.Text
size="small"
color={isHeading ? undefined : 'muted'}
className={styles.rowLabel}
>
{label}
</Typography.Text>
{value !== undefined && (
<Typography.Text size="small" className={styles.rowValue}>
{value}
</Typography.Text>
)}
</div>
);
}
export default TraceTooltipRow;

View File

@@ -1,15 +0,0 @@
import { ReactNode } from 'react';
import styles from './TraceTooltip.module.scss';
interface TraceTooltipSectionProps {
children: ReactNode;
}
function TraceTooltipSection({
children,
}: TraceTooltipSectionProps): JSX.Element {
return <div className={styles.section}>{children}</div>;
}
export default TraceTooltipSection;

View File

@@ -203,9 +203,8 @@
.statusCell {
display: flex;
align-items: center;
justify-content: flex-start;
justify-content: center;
cursor: pointer;
padding-left: var(--spacing-2);
--badge-border-radius: 2px;
--badge-padding: 3px 6px;
@@ -395,11 +394,6 @@
}
}
.aiSpanIcon {
flex-shrink: 0;
margin: 0 var(--spacing-2);
}
.treeLabel {
color: var(--l1-foreground);
font-family: 'Inter';

View File

@@ -20,6 +20,7 @@ import {
} from '@tanstack/react-table';
import { useVirtualizer, Virtualizer } from '@tanstack/react-virtual';
import cx from 'classnames';
import HttpStatusBadge from 'components/HttpStatusBadge/HttpStatusBadge';
import TimelineV3 from 'components/TimelineV3/TimelineV3';
import { convertTimeToRelevantUnit } from 'utils/traceUtils';
import { useCopySpanLink } from 'hooks/trace/useCopySpanLink';
@@ -27,13 +28,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { colorToRgb } from 'lib/uPlotLib/utils/generateColor';
import {
ChevronDown,
ChevronRight,
Link,
ListPlus,
Sparkle,
} from '@signozhq/icons';
import { ChevronDown, ChevronRight, Link, ListPlus } from '@signozhq/icons';
import { useTraceStore } from 'pages/TraceDetailsV3/stores/traceStore';
import { resolveSpanColor } from 'pages/TraceDetailsV3/utils';
import { useBoundaryPagination } from 'pages/TraceDetailsV3/TraceWaterfall/hooks/useBoundaryPagination';
@@ -44,8 +39,6 @@ import { toFixed } from 'utils/toFixed';
import { EventTooltipContent } from '../../../SpanHoverCard/EventTooltipContent';
import { SpanHoverCard } from '../../../SpanHoverCard/SpanHoverCard';
import SpanHintBadge from '../../../SpanHintBadge/SpanHintBadge';
import { isAiSpan } from '../../../utils/aiTraceUtils';
import AddSpanToFunnelModal from '../../AddSpanToFunnelModal/AddSpanToFunnelModal';
import { IInterestedSpan } from '../../types';
@@ -325,23 +318,14 @@ const SpanOverview = memo(function SpanOverview({
</span>
{/* Colored service dot */}
{isAiSpan(span) ? (
<Sparkle
size={10}
color={effectiveColor}
fill={effectiveColor}
className={styles.aiSpanIcon}
/>
) : (
<span
className={cx(styles.treeIcon, { [styles.hasError]: span.has_error })}
style={
{
'--service-dot-color': effectiveColor,
} as React.CSSProperties
}
/>
)}
<span
className={cx(styles.treeIcon, { [styles.hasError]: span.has_error })}
style={
{
'--service-dot-color': effectiveColor,
} as React.CSSProperties
}
/>
{/* Span name + service name */}
<span className={styles.treeLabel}>
@@ -960,7 +944,9 @@ function Success(props: ISuccessProps): JSX.Element {
onMouseLeave={(): void => applyHoverClass(null)}
onClick={(): void => handleSpanClick(span)}
>
<SpanHintBadge span={span} />
{span.response_status_code && (
<HttpStatusBadge statusCode={span.response_status_code} />
)}
</div>
);
})}

View File

@@ -1,20 +0,0 @@
.tooltipBody {
display: flex;
flex-direction: column;
letter-spacing: var(--paragraph-small-400-letter-spacing);
color: var(--l1-foreground-hover);
}
.tooltipTitle {
font-family: 'SF Mono', 'Geist Mono', monospace;
word-break: break-all;
}
.tooltipSurface {
background-color: color-mix(in srgb, var(--l3-background) 60%, transparent);
border: 1px solid var(--l2-border);
border-radius: var(--radius-2);
padding: var(--spacing-4);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
}

View File

@@ -46,8 +46,6 @@ import {
traceDetailFieldValues,
traceFlamegraphResponse,
traceSummaryResponse,
TRACE_SCENARIOS,
type TraceScenario,
traceWaterfallResponse,
} from './__story_mockdata__/traceDetails';
@@ -105,13 +103,6 @@ const spanLogScopeOf = (body: QueryRangeRequestV5): SpanLogScope => {
export const traceDetailsMocks = defineStoryMocks({
controls: {
scenario: choiceControl<TraceScenario>('Scenario', {
group: TRACE,
description:
'The spans the trace is built from: a checkout, one span per HTTP status badge, or an agent run with LLM, tool and agent spans and the AI totals in the header.',
options: TRACE_SCENARIOS,
value: 'checkout',
}),
spans: countControl('Spans', {
group: TRACE,
description:
@@ -160,7 +151,6 @@ export const traceDetailsMocks = defineStoryMocks({
},
handlers: (values, response) => {
const trace = {
scenario: values.scenario,
spans: values.spans,
errors: values.errors,
traceStart: traceStartOf(),
@@ -298,7 +288,7 @@ export const traceDetailsMocks = defineStoryMocks({
},
config: (values) => ({
route: values.spanPanel
? `/trace/${STORY_TRACE_ID}?spanId=${spanIdAt(focusSpanIndex(values.scenario, values.spans))}`
? `/trace/${STORY_TRACE_ID}?spanId=${spanIdAt(focusSpanIndex(values.spans))}`
: `/trace/${STORY_TRACE_ID}`,
}),
effect: (values) => {

View File

@@ -13,12 +13,7 @@ import {
import { storyMocks } from '@/storybook/controls/defineStoryMocks';
import type { PageStoryArgs } from '@/storybook/runtime/resolveStory';
import {
focusSpanIndex,
scenarioSpanCount,
spanIdAt,
STORY_TRACE_ID,
} from './__story_mockdata__/traceDetails';
import { STORY_TRACE_ID } from './__story_mockdata__/traceDetails';
import {
openRowMenu,
startDownload,
@@ -101,40 +96,6 @@ export const BottomStripLoading: Story = {
args: { bottomStrip: true, dataState: 'loading' },
};
/**
* One span per HTTP status badge the waterfall draws: informational, success,
* redirect, client and server errors, and a span with no status code and so no
* badge.
*/
export const StatusCodes: Story = {
args: {
scenario: 'status-codes',
spans: scenarioSpanCount('status-codes'),
spanPanel: false,
},
};
const AI_SPANS = scenarioSpanCount('ai');
/**
* An agent run: LLM, tool and agent badges with sparkle icons in the waterfall,
* token and cost totals in the header, the span panel on an LLM call with its
* tokens and cost, and that span's hover card open on its usage breakdown.
*/
export const AiTrace: Story = {
args: { scenario: 'ai', spans: AI_SPANS },
play: async ({ canvasElement }): Promise<void> => {
const row = await within(canvasElement).findByTestId(
`cell-0-${spanIdAt(focusSpanIndex('ai', AI_SPANS))}`,
undefined,
untilLoaded,
);
await fireEvent.mouseOver(row);
await screen.findByText('Usage Breakdown', undefined, untilLoaded);
},
};
/**
* Turns on Highlight errors, which writes `has_error = true` into the filter and
* expands the bar to show it.

View File

@@ -23,10 +23,6 @@ interface SpanTemplate {
name: string;
kind: string;
failing?: boolean;
/** The span the panel opens on when nothing fails. */
focus?: boolean;
/** `response_status_code`; failing spans default to 503, the rest to 200. */
statusCode?: string;
attributes: Record<string, string | number | boolean>;
}
@@ -127,145 +123,6 @@ const SPAN_TEMPLATES: SpanTemplate[] = [
},
];
/** One span per badge colour, plus a span with no status code and so no badge. */
const STATUS_CODE_TEMPLATES: SpanTemplate[] = [
['200', 'GET /status/ok'],
['101', 'GET /ws/upgrade'],
['201', 'POST /status/created'],
['204', 'DELETE /status/no-content'],
['301', 'GET /status/moved'],
['304', 'GET /status/not-modified'],
['400', 'POST /status/bad-request'],
['401', 'GET /status/unauthorized'],
['404', 'GET /status/not-found'],
['429', 'GET /status/rate-limited'],
['500', 'GET /status/internal-error'],
['502', 'GET /status/bad-gateway'],
['503', 'GET /status/unavailable'],
['504', 'GET /status/timeout'],
['', 'internal.compute'],
].map(
([statusCode, name]): SpanTemplate => ({
service: 'status-api',
name,
kind: statusCode ? 'Server' : 'Internal',
failing: Number(statusCode) >= 500,
statusCode,
attributes: statusCode
? {
'http.method': name.split(' ')[0],
'http.status_code': Number(statusCode),
}
: {},
}),
);
/**
* An agent run: agent, LLM and tool spans, each badged by kind. The LLM spans
* cover cache counted inside input (OpenAI), cache counted beside it
* (Anthropic), reasoning, and an embedding with input only.
*/
const AI_TEMPLATES: SpanTemplate[] = [
{
service: 'support-agent',
name: 'invoke_agent support-agent',
kind: 'Internal',
attributes: { 'gen_ai.agent.name': 'support-agent' },
},
{
service: 'support-agent',
name: 'chat gpt-4o',
kind: 'Client',
attributes: {
'gen_ai.request.model': 'gpt-4o-2024-08-06',
'gen_ai.usage.input_tokens': 1840,
'gen_ai.usage.output_tokens': 212,
'gen_ai.usage.cache_read.input_tokens': 1024,
'gen_ai.usage.cache_creation.input_tokens': 0,
'signoz.gen_ai.usage.tokens.cost': 0.00566,
},
},
{
service: 'support-agent',
name: 'execute_tool search_docs',
kind: 'Internal',
attributes: { 'gen_ai.tool.name': 'search_docs' },
},
{
service: 'support-agent',
name: 'chat claude-sonnet-4',
kind: 'Client',
focus: true,
attributes: {
'gen_ai.request.model': 'claude-sonnet-4-20250514',
'gen_ai.usage.input_tokens': 86,
'gen_ai.usage.output_tokens': 640,
'gen_ai.usage.cache_read.input_tokens': 4200,
'gen_ai.usage.cache_creation.input_tokens': 1300,
'signoz.gen_ai.usage.tokens.cost': 0.01853,
},
},
{
service: 'docs-api',
name: 'GET /docs/search',
kind: 'Server',
attributes: {
'http.method': 'GET',
'http.route': '/docs/search',
'http.status_code': 200,
},
},
{
service: 'support-agent',
name: 'execute_tool fetch_ticket',
kind: 'Internal',
failing: true,
attributes: { 'gen_ai.tool.name': 'fetch_ticket' },
},
{
service: 'support-agent',
name: 'invoke_agent triage-agent',
kind: 'Internal',
attributes: { 'gen_ai.agent.name': 'triage-agent' },
},
{
service: 'support-agent',
name: 'embeddings text-embedding-3-small',
kind: 'Client',
attributes: {
'gen_ai.request.model': 'text-embedding-3-small',
'gen_ai.usage.input_tokens': 312,
'signoz.gen_ai.usage.tokens.cost': 0.00000624,
},
},
{
service: 'support-agent',
name: 'chat o3-mini',
kind: 'Client',
attributes: {
'gen_ai.request.model': 'o3-mini-2025-01-31',
'gen_ai.usage.input_tokens': 920,
'gen_ai.usage.output_tokens': 1480,
'gen_ai.usage.reasoning.output_tokens': 1152,
'signoz.gen_ai.usage.tokens.cost': 0.007524,
},
},
];
export const TRACE_SCENARIOS = ['checkout', 'status-codes', 'ai'] as const;
export type TraceScenario = (typeof TRACE_SCENARIOS)[number];
const SCENARIO_TEMPLATES: Record<TraceScenario, SpanTemplate[]> = {
checkout: SPAN_TEMPLATES,
'status-codes': STATUS_CODE_TEMPLATES,
ai: AI_TEMPLATES,
};
/** Spans it takes for the scenario to show every template once. */
export const scenarioSpanCount = (scenario: TraceScenario): number =>
SCENARIO_TEMPLATES[scenario].length;
const TRACE_SERVICES = [
...new Set(SPAN_TEMPLATES.map(({ service }) => service)),
];
@@ -289,7 +146,6 @@ interface BuiltSpan {
}
interface TraceOptions {
scenario: TraceScenario;
spans: number;
errors: boolean;
/** Epoch milliseconds the root span starts at. */
@@ -300,33 +156,24 @@ export const spanIdAt = (index: number): string =>
`b17e${index.toString(16).padStart(12, '0')}`;
/**
* The span the story opens the panel on: the scenario's focus span, else the
* first failing one, so its events and error styling are on screen, and the
* root when neither is in the trace.
* The span the story opens the panel on: the first failing one, so its events
* and error styling are on screen, and the root when nothing fails.
*/
export const focusSpanIndex = (
scenario: TraceScenario,
spans: number,
): number => {
const templates = SCENARIO_TEMPLATES[scenario];
const focus = templates.findIndex((template) => template.focus);
const failing = templates.findIndex((template) => template.failing);
const index = focus >= 0 ? focus : failing;
export const focusSpanIndex = (spans: number): number => {
const failing = SPAN_TEMPLATES.findIndex(({ failing }) => failing);
return index >= 0 && index < spans ? index : 0;
return failing >= 0 && failing < spans ? failing : 0;
};
const buildSpans = ({
scenario,
spans,
errors,
traceStart,
}: TraceOptions): BuiltSpan[] => {
const built: BuiltSpan[] = [];
const templates = SCENARIO_TEMPLATES[scenario];
for (let index = 0; index < spans; index += 1) {
const template = templates[index % templates.length];
const template = SPAN_TEMPLATES[index % SPAN_TEMPLATES.length];
const parent = index === 0 ? undefined : built[parentIndexOf(index)];
// Children fit inside their parent's window, second child after the first,
// so no span outlives its parent and no two siblings overlap.
@@ -410,8 +257,7 @@ const waterfallSpan = (span: BuiltSpan): SpantypesWaterfallSpanDTO => ({
status_code: span.hasError ? 2 : 1,
status_code_string: span.hasError ? 'Error' : 'Ok',
status_message: span.hasError ? 'upstream unavailable' : '',
response_status_code:
span.template.statusCode ?? (span.hasError ? '503' : '200'),
response_status_code: span.hasError ? '503' : '200',
has_children: span.hasChildren,
sub_tree_node_count: span.subTreeNodeCount,
level: span.level,
@@ -458,36 +304,6 @@ export const traceWaterfallResponse = (
};
};
const numberAttribute = (span: BuiltSpan, key: string): number =>
Number(span.template.attributes[key] ?? 0);
/** The trace's AI totals, summed over its spans the way the backend does. */
const traceAiSummary = (
spans: BuiltSpan[],
): GetTraceSummary200['data']['ai'] => {
const aiSpans = spans.filter(({ template }) =>
Object.keys(template.attributes).some((key) => key.includes('gen_ai.')),
);
if (aiSpans.length === 0) {
return undefined;
}
const sum = (key: string): number =>
aiSpans.reduce((total, span) => total + numberAttribute(span, key), 0);
return {
tokens: {
input: sum('gen_ai.usage.input_tokens'),
output: sum('gen_ai.usage.output_tokens'),
cacheRead: sum('gen_ai.usage.cache_read.input_tokens'),
cacheWrite: sum('gen_ai.usage.cache_creation.input_tokens'),
reasoning: sum('gen_ai.usage.reasoning.output_tokens'),
},
totalCost: sum('signoz.gen_ai.usage.tokens.cost'),
};
};
export const traceSummaryResponse = (
options: TraceOptions & { missingSpans: boolean },
): GetTraceSummary200 => {
@@ -505,7 +321,6 @@ export const traceSummaryResponse = (
totalSpansCount: spans.length,
totalErrorSpansCount: spans.filter(({ hasError }) => hasError).length,
hasMissingSpans: options.missingSpans,
ai: traceAiSummary(spans),
},
};
};
@@ -657,7 +472,7 @@ export const spanLogRows = (
): RawRow[] => {
const step = ROOT_DURATION_MS / Math.max(count, 1);
const spans = buildSpans(options);
const focus = spans[focusSpanIndex(options.scenario, options.spans)];
const focus = spans[focusSpanIndex(options.spans)];
return Array.from({ length: count }, (_, index) => {
const at = options.traceStart + SCOPE_OFFSET_MS[scope] + index * step;

View File

@@ -9,14 +9,9 @@ import {
RESERVED_ERROR,
} from './utils/generateColorPair';
/** Like `||`, but keeps `0`. */
const valueOrUndefined = <T>(value: T): T | undefined =>
value || value === 0 ? value : undefined;
/**
* Look up an attribute from both `resource` and `attributes` on a span.
* Resources are checked first (service.name, k8s.* etc. live there).
* Falsy values count as missing, except `0`.
*
* Accepts both `SpanV3` (waterfall) and `FlamegraphSpan` (flamegraph) by typing
* structurally — the only fields touched are `resource` and `attributes`.
@@ -27,14 +22,11 @@ export function getSpanAttribute(
span: {
resource?: Record<string, string>;
attributes?: Record<string, any>;
tagMap?: Record<string, any>;
},
key: string,
): string | undefined {
return (
valueOrUndefined(span.resource?.[key]) ??
valueOrUndefined(span.attributes?.[key]) ??
valueOrUndefined(span.tagMap?.[key])
span.resource?.[key] || span.attributes?.[key] || (span as any).tagMap?.[key]
);
}

View File

@@ -1,51 +0,0 @@
import { SpanV3 } from 'types/api/trace/getTraceV3';
import {
AiSpanKind,
getAiSpanKind,
getTotalInputTokens,
isAiSpan,
} from '../aiTraceUtils';
const spanWith = (attributes: Record<string, string | number>): SpanV3 =>
({ attributes, resource: {} }) as unknown as SpanV3;
describe('getAiSpanKind', () => {
it.each<[Record<string, string | number>, AiSpanKind]>([
[{ 'gen_ai.request.model': 'gpt-4o' }, AiSpanKind.Llm],
[{ 'gen_ai.usage.input_tokens': 0 }, AiSpanKind.Llm],
[{ 'gen_ai.usage.reasoning.output_tokens': 12 }, AiSpanKind.Llm],
[{ 'gen_ai.tool.name': 'search' }, AiSpanKind.Tool],
[{ 'gen_ai.agent.name': 'planner' }, AiSpanKind.Agent],
[
{ 'gen_ai.tool.name': 'search', 'gen_ai.request.model': 'gpt-4o' },
AiSpanKind.Tool,
],
[
{ 'gen_ai.agent.name': 'planner', 'gen_ai.usage.input_tokens': 10 },
AiSpanKind.Agent,
],
])('classifies %p as %s', (attributes, kind) => {
expect(getAiSpanKind(spanWith(attributes))).toBe(kind);
});
it('returns undefined for non-AI spans and empty values', () => {
expect(getAiSpanKind(spanWith({ 'http.method': 'GET' }))).toBeUndefined();
expect(getAiSpanKind(spanWith({ 'gen_ai.tool.name': '' }))).toBeUndefined();
expect(isAiSpan(spanWith({}))).toBe(false);
});
});
describe('getTotalInputTokens', () => {
it.each([
// Cache reported inside input (OpenAI): input already includes it.
[{ input: 1000, cacheRead: 600, cacheWrite: 100 }, 1000],
// Cache reported separately (Anthropic): add it.
[{ input: 50, cacheRead: 600, cacheWrite: 100 }, 750],
[{ input: 71, cacheRead: 0, cacheWrite: 0 }, 71],
[{ input: 10 }, 10],
[{ cacheRead: 600 }, 600],
])('%p → %p', (tokens, expected) => {
expect(getTotalInputTokens(tokens)).toBe(expected);
});
});

View File

@@ -1,168 +0,0 @@
import { SpanV3 } from 'types/api/trace/getTraceV3';
import { getSpanAttribute } from 'pages/TraceDetailsV3/utils';
export const GEN_AI_KEYS = {
requestModel: 'gen_ai.request.model',
toolName: 'gen_ai.tool.name',
agentName: 'gen_ai.agent.name',
inputTokens: 'gen_ai.usage.input_tokens',
outputTokens: 'gen_ai.usage.output_tokens',
cacheReadTokens: 'gen_ai.usage.cache_read.input_tokens',
cacheCreationTokens: 'gen_ai.usage.cache_creation.input_tokens',
reasoningTokens: 'gen_ai.usage.reasoning.output_tokens',
cost: 'signoz.gen_ai.usage.tokens.cost',
} as const;
export enum AiSpanKind {
Llm = 'llm',
Tool = 'tool',
Agent = 'agent',
}
const LLM_KEYS = [
GEN_AI_KEYS.requestModel,
GEN_AI_KEYS.inputTokens,
GEN_AI_KEYS.outputTokens,
GEN_AI_KEYS.cacheReadTokens,
GEN_AI_KEYS.cacheCreationTokens,
GEN_AI_KEYS.reasoningTokens,
];
function hasAttribute(span: SpanV3, key: string): boolean {
return getSpanAttribute(span, key) !== undefined;
}
/** Tool and agent spans may also carry model or usage keys, so they win over LLM. */
export function getAiSpanKind(span: SpanV3): AiSpanKind | undefined {
if (hasAttribute(span, GEN_AI_KEYS.toolName)) {
return AiSpanKind.Tool;
}
if (hasAttribute(span, GEN_AI_KEYS.agentName)) {
return AiSpanKind.Agent;
}
if (LLM_KEYS.some((key) => hasAttribute(span, key))) {
return AiSpanKind.Llm;
}
return undefined;
}
export function isAiSpan(span: SpanV3): boolean {
return getAiSpanKind(span) !== undefined;
}
/** Token counts; `null` or missing means the backend did not send the value. */
export interface AiTokenCounts {
input?: number;
output?: number;
cacheRead?: number;
cacheWrite?: number;
reasoning?: number;
}
export function hasValue(value: number | null | undefined): value is number {
return value !== undefined && value !== null;
}
/**
* Input including cache read/write, counted once. Providers that report cache
* inside `input` (OpenAI) have `input >= cache`; those that report it
* separately (Anthropic) usually have `input < cache`.
*/
export function getTotalInputTokens({
input,
cacheRead,
cacheWrite,
}: AiTokenCounts): number {
const inputValue = input ?? 0;
const cache = (cacheRead ?? 0) + (cacheWrite ?? 0);
return inputValue < cache ? inputValue + cache : inputValue;
}
/** `Input Token` for a count of 1, `Input Tokens` otherwise. */
export function getTokenLabel(
prefix: string,
count: number | null | undefined,
): string {
return `${prefix} ${count === 1 ? 'Token' : 'Tokens'}`;
}
/** Missing values render as `0`. */
export function formatTokens(value: number | null | undefined): string {
return hasValue(value) ? value.toLocaleString('en-US') : '0';
}
export function formatCost(value: number): string {
return `$ ${value.toLocaleString('en-US', { maximumFractionDigits: 10 })}`;
}
export interface SpanAiUsage {
inputTokens?: number;
outputTokens?: number;
cacheReadTokens?: number;
cacheCreationTokens?: number;
reasoningTokens?: number;
cost?: number;
}
export interface SpanAiDetails {
model?: string;
toolName?: string;
agentName?: string;
usage?: SpanAiUsage;
}
function getGenAiAttributeAsString(
span: SpanV3,
key: string,
): string | undefined {
const value = getSpanAttribute(span, key);
return value === undefined ? undefined : String(value);
}
function getGenAiAttributeAsNumber(
span: SpanV3,
key: string,
): number | undefined {
const value = getSpanAttribute(span, key);
if (value === undefined) {
return undefined;
}
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
export function getAIUsageDetails(span: SpanV3): SpanAiDetails | undefined {
const usage: SpanAiUsage = {
inputTokens: getGenAiAttributeAsNumber(span, GEN_AI_KEYS.inputTokens),
outputTokens: getGenAiAttributeAsNumber(span, GEN_AI_KEYS.outputTokens),
cacheReadTokens: getGenAiAttributeAsNumber(span, GEN_AI_KEYS.cacheReadTokens),
cacheCreationTokens: getGenAiAttributeAsNumber(
span,
GEN_AI_KEYS.cacheCreationTokens,
),
reasoningTokens: getGenAiAttributeAsNumber(span, GEN_AI_KEYS.reasoningTokens),
cost: getGenAiAttributeAsNumber(span, GEN_AI_KEYS.cost),
};
const hasUsage = Object.values(usage).some((value) => value !== undefined);
const details: SpanAiDetails = {
model: getGenAiAttributeAsString(span, GEN_AI_KEYS.requestModel),
toolName: getGenAiAttributeAsString(span, GEN_AI_KEYS.toolName),
agentName: getGenAiAttributeAsString(span, GEN_AI_KEYS.agentName),
usage: hasUsage ? usage : undefined,
};
return details.model || details.toolName || details.agentName || hasUsage
? details
: undefined;
}
export function convertToAiTokens(usage: SpanAiUsage): AiTokenCounts {
return {
input: usage.inputTokens,
output: usage.outputTokens,
cacheRead: usage.cacheReadTokens,
cacheWrite: usage.cacheCreationTokens,
reasoning: usage.reasoningTokens,
};
}

View File

@@ -21,8 +21,6 @@ export interface QueryData {
queryName: string;
legend?: string;
values: [number, string][];
/** Epoch-second x values (as in `values`) of buckets that don't cover a full step. */
partialTimestamps?: number[];
quantity?: number[];
unit?: string;
table?: {
@@ -47,7 +45,7 @@ export interface SeriesItem {
[key: string]: string;
};
labelsArray: { [key: string]: string }[];
values: { timestamp: number; value: string; partial?: boolean }[];
values: { timestamp: number; value: string }[];
metaData?: {
alias: string;
index: number;

View File

@@ -4,16 +4,14 @@ import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
"github.com/gorilla/mux"
)
func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/infra_monitoring/hosts", handler.New(
provider.authzMiddleware.CheckResources(provider.infraMonitoringHandler.ListHosts, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListHosts),
handler.OpenAPIDef{
ID: "ListHosts",
Tags: []string{"inframonitoring"},
@@ -26,18 +24,13 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}),
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
Selector: querybuilder.TelemetrySelector,
Resources: querybuilder.BuilderQueryWithFilterExpressionResources(coretypes.ResourceTelemetryResourceMetrics, "filter.expression"),
}))).Methods(http.MethodPost).GetError(); err != nil {
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/infra_monitoring/pods", handler.New(
provider.authzMiddleware.CheckResources(provider.infraMonitoringHandler.ListPods, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListPods),
handler.OpenAPIDef{
ID: "ListPods",
Tags: []string{"inframonitoring"},
@@ -50,18 +43,13 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}),
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
Selector: querybuilder.TelemetrySelector,
Resources: querybuilder.BuilderQueryWithFilterExpressionResources(coretypes.ResourceTelemetryResourceMetrics, "filter.expression"),
}))).Methods(http.MethodPost).GetError(); err != nil {
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/infra_monitoring/kube_containers", handler.New(
provider.authzMiddleware.CheckResources(provider.infraMonitoringHandler.ListContainers, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListContainers),
handler.OpenAPIDef{
ID: "ListContainers",
Tags: []string{"inframonitoring"},
@@ -74,18 +62,13 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}),
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
Selector: querybuilder.TelemetrySelector,
Resources: querybuilder.BuilderQueryWithFilterExpressionResources(coretypes.ResourceTelemetryResourceMetrics, "filter.expression"),
}))).Methods(http.MethodPost).GetError(); err != nil {
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/infra_monitoring/nodes", handler.New(
provider.authzMiddleware.CheckResources(provider.infraMonitoringHandler.ListNodes, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListNodes),
handler.OpenAPIDef{
ID: "ListNodes",
Tags: []string{"inframonitoring"},
@@ -98,18 +81,13 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}),
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
Selector: querybuilder.TelemetrySelector,
Resources: querybuilder.BuilderQueryWithFilterExpressionResources(coretypes.ResourceTelemetryResourceMetrics, "filter.expression"),
}))).Methods(http.MethodPost).GetError(); err != nil {
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/infra_monitoring/namespaces", handler.New(
provider.authzMiddleware.CheckResources(provider.infraMonitoringHandler.ListNamespaces, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListNamespaces),
handler.OpenAPIDef{
ID: "ListNamespaces",
Tags: []string{"inframonitoring"},
@@ -122,18 +100,13 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}),
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
Selector: querybuilder.TelemetrySelector,
Resources: querybuilder.BuilderQueryWithFilterExpressionResources(coretypes.ResourceTelemetryResourceMetrics, "filter.expression"),
}))).Methods(http.MethodPost).GetError(); err != nil {
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/infra_monitoring/clusters", handler.New(
provider.authzMiddleware.CheckResources(provider.infraMonitoringHandler.ListClusters, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListClusters),
handler.OpenAPIDef{
ID: "ListClusters",
Tags: []string{"inframonitoring"},
@@ -146,18 +119,13 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}),
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
Selector: querybuilder.TelemetrySelector,
Resources: querybuilder.BuilderQueryWithFilterExpressionResources(coretypes.ResourceTelemetryResourceMetrics, "filter.expression"),
}))).Methods(http.MethodPost).GetError(); err != nil {
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/infra_monitoring/pvcs", handler.New(
provider.authzMiddleware.CheckResources(provider.infraMonitoringHandler.ListVolumes, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListVolumes),
handler.OpenAPIDef{
ID: "ListVolumes",
Tags: []string{"inframonitoring"},
@@ -170,18 +138,13 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}),
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
Selector: querybuilder.TelemetrySelector,
Resources: querybuilder.BuilderQueryWithFilterExpressionResources(coretypes.ResourceTelemetryResourceMetrics, "filter.expression"),
}))).Methods(http.MethodPost).GetError(); err != nil {
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/infra_monitoring/deployments", handler.New(
provider.authzMiddleware.CheckResources(provider.infraMonitoringHandler.ListDeployments, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListDeployments),
handler.OpenAPIDef{
ID: "ListDeployments",
Tags: []string{"inframonitoring"},
@@ -194,18 +157,13 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}),
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
Selector: querybuilder.TelemetrySelector,
Resources: querybuilder.BuilderQueryWithFilterExpressionResources(coretypes.ResourceTelemetryResourceMetrics, "filter.expression"),
}))).Methods(http.MethodPost).GetError(); err != nil {
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/infra_monitoring/statefulsets", handler.New(
provider.authzMiddleware.CheckResources(provider.infraMonitoringHandler.ListStatefulSets, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListStatefulSets),
handler.OpenAPIDef{
ID: "ListStatefulSets",
Tags: []string{"inframonitoring"},
@@ -218,18 +176,13 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}),
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
Selector: querybuilder.TelemetrySelector,
Resources: querybuilder.BuilderQueryWithFilterExpressionResources(coretypes.ResourceTelemetryResourceMetrics, "filter.expression"),
}))).Methods(http.MethodPost).GetError(); err != nil {
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/infra_monitoring/jobs", handler.New(
provider.authzMiddleware.CheckResources(provider.infraMonitoringHandler.ListJobs, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListJobs),
handler.OpenAPIDef{
ID: "ListJobs",
Tags: []string{"inframonitoring"},
@@ -242,18 +195,13 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}),
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
Selector: querybuilder.TelemetrySelector,
Resources: querybuilder.BuilderQueryWithFilterExpressionResources(coretypes.ResourceTelemetryResourceMetrics, "filter.expression"),
}))).Methods(http.MethodPost).GetError(); err != nil {
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/infra_monitoring/daemonsets", handler.New(
provider.authzMiddleware.CheckResources(provider.infraMonitoringHandler.ListDaemonSets, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListDaemonSets),
handler.OpenAPIDef{
ID: "ListDaemonSets",
Tags: []string{"inframonitoring"},
@@ -266,18 +214,13 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}),
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
Selector: querybuilder.TelemetrySelector,
Resources: querybuilder.BuilderQueryWithFilterExpressionResources(coretypes.ResourceTelemetryResourceMetrics, "filter.expression"),
}))).Methods(http.MethodPost).GetError(); err != nil {
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/infra_monitoring/checks", handler.New(
provider.authzMiddleware.CheckResources(provider.infraMonitoringHandler.GetChecks, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.GetChecks),
handler.OpenAPIDef{
ID: "GetChecks",
Tags: []string{"inframonitoring"},
@@ -289,13 +232,8 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead)}),
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
Selector: querybuilder.TelemetrySelector,
Resources: querybuilder.BuilderQueryWildcardResources(coretypes.ResourceTelemetryResourceMetrics),
}))).Methods(http.MethodGet).GetError(); err != nil {
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}

View File

@@ -84,26 +84,5 @@ func (provider *provider) addPromoteRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/promoted_paths/materialized", handler.New(provider.authzMiddleware.CheckResources(provider.promoteHandler.IndexMaterializedPaths, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName), handler.OpenAPIDef{
ID: "IndexMaterializedPaths",
Tags: []string{"promote"},
Summary: "Index materialized paths",
Description: "This endpoint indexes the JSON sub-columns of materialized paths whose columns are indexed and returns the indexed and skipped paths.",
Request: new(promotetypes.PostableIndexMaterializedPaths),
RequestContentType: "application/json",
Response: new(promotetypes.IndexMaterializedPathsResult),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLogsField.Scope(coretypes.VerbUpdate), coretypes.ResourceMetaResourceTracesField.Scope(coretypes.VerbUpdate)}),
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
Verb: coretypes.VerbUpdate,
Category: coretypes.ActionCategoryConfigurationChange,
Selector: coretypes.WildcardSelector,
Resources: promotetypes.IndexMaterializedPathsResources,
}))).Methods(http.MethodPost).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -72,23 +72,3 @@ func (h *handler) ListPromotedPaths(w http.ResponseWriter, r *http.Request) {
render.Success(w, http.StatusOK, paths)
}
func (h *handler) IndexMaterializedPaths(w http.ResponseWriter, r *http.Request) {
var req promotetypes.PostableIndexMaterializedPaths
if err := binding.JSON.BindBody(r.Body, &req); err != nil {
render.Error(w, err)
return
}
if err := req.Validate(); err != nil {
render.Error(w, err)
return
}
paths, err := h.module.IndexMaterializedPaths(r.Context(), req)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, paths)
}

View File

@@ -2,12 +2,10 @@ package implpromote
import (
"context"
"log/slog"
"strings"
schemamigrator "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/promote"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
@@ -24,15 +22,10 @@ var (
type module struct {
metadataStore telemetrytypes.MetadataStore
telemetryStore telemetrystore.TelemetryStore
settings factory.ScopedProviderSettings
}
func NewModule(metadataStore telemetrytypes.MetadataStore, telemetrystore telemetrystore.TelemetryStore, providerSettings factory.ProviderSettings) promote.Module {
return &module{
metadataStore: metadataStore,
telemetryStore: telemetrystore,
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/promote/implpromote"),
}
func NewModule(metadataStore telemetrytypes.MetadataStore, telemetrystore telemetrystore.TelemetryStore) promote.Module {
return &module{metadataStore: metadataStore, telemetryStore: telemetrystore}
}
func (m *module) ListPromotedPaths(ctx context.Context, filters promotetypes.ListPromotedPathsFilters) ([]promotetypes.PromotePath, error) {
@@ -108,71 +101,6 @@ func (m *module) listPromotedPaths(ctx context.Context, target promotetypes.Targ
return response, nil
}
func (m *module) IndexMaterializedPaths(ctx context.Context, req promotetypes.PostableIndexMaterializedPaths) (*promotetypes.IndexMaterializedPathsResult, error) {
target, err := req.Target()
if err != nil {
return nil, err
}
keys, err := m.metadataStore.GetMaterializedKeys(ctx, target.Entry.Signal)
if err != nil {
return nil, err
}
paths := []*promotetypes.PromotePath{}
names := []string{}
for _, key := range keys {
if key.FieldContext != target.Entry.FieldContext || len(key.Indexes) == 0 {
continue
}
path, err := promotetypes.NewMaterializedPromotePath(target, key)
if err != nil {
m.settings.Logger().WarnContext(ctx, "skipping materialized path that cannot be indexed", slog.String("path", key.Name), errors.Attr(err))
continue
}
paths = append(paths, path)
names = append(names, path.Path)
}
result := &promotetypes.IndexMaterializedPathsResult{Indexed: []*promotetypes.PromotePath{}, Skipped: []*promotetypes.PromotePath{}}
if len(paths) > 0 {
existing, err := m.metadataStore.ListJSONIndexes(ctx, target.JSONIndexLookup(), names...)
if err != nil {
return nil, err
}
existingByPath := map[string][]promotetypes.WrappedIndex{}
for _, index := range existing {
existingByPath[index.Name] = append(existingByPath[index.Name], promotetypes.WrappedIndex{FieldDataType: index.FieldDataType, Type: index.IndexType, Granularity: index.Granularity})
}
for _, path := range paths {
if indexes, ok := existingByPath[path.Path]; ok {
path.Indexes = indexes
result.Skipped = append(result.Skipped, path)
continue
}
result.Indexed = append(result.Indexed, path)
}
}
if !req.DryRun && len(result.Indexed) > 0 {
if err := m.promotePaths(ctx, target, result.Indexed...); err != nil {
return nil, err
}
}
indexed := make([]string, 0, len(result.Indexed))
for _, path := range result.Indexed {
indexed = append(indexed, path.Path)
}
skipped := make([]string, 0, len(result.Skipped))
for _, path := range result.Skipped {
skipped = append(skipped, path.Path)
}
m.settings.Logger().InfoContext(ctx, "indexed materialized paths", slog.String("signal", req.Signal), slog.Bool("dry_run", req.DryRun), slog.Int("indexed_count", len(indexed)), slog.Any("indexed", indexed), slog.Int("skipped_count", len(skipped)), slog.Any("skipped", skipped))
return result, nil
}
func (m *module) PromotePaths(ctx context.Context, paths ...*promotetypes.PromotePath) error {
byTarget := map[promotetypes.Target][]*promotetypes.PromotePath{}
targets := []promotetypes.Target{}

View File

@@ -9,7 +9,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/SigNoz/signoz/pkg/factory/factorytest"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
"github.com/SigNoz/signoz/pkg/types/promotetypes"
@@ -72,7 +71,7 @@ func TestPromotePaths(t *testing.T) {
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
store := telemetrytypestest.NewMockMetadataStore()
m := NewModule(store, nil, factorytest.NewSettings())
m := NewModule(store, nil)
err := m.PromotePaths(ctx, testCase.paths...)
if testCase.wantErr {
@@ -195,7 +194,7 @@ func TestPromotePathsCreatesIndexes(t *testing.T) {
ts := telemetrystoretest.New(telemetrystore.Config{}, sqlmock.QueryMatcherRegexp)
store := telemetrytypestest.NewMockMetadataStore()
store.PromotedPathsMap[telemetrytypes.SignalLogs] = testCase.promoted
m := NewModule(store, ts, factorytest.NewSettings())
m := NewModule(store, ts)
ts.Mock().ExpectExec("ADD INDEX (.+)" + regexp.QuoteMeta(testCase.wantDDLColumn)).WillReturnError(nil)
require.NoError(t, m.PromotePaths(ctx, testCase.path))
@@ -391,7 +390,7 @@ func TestListPromotedPaths(t *testing.T) {
store := telemetrytypestest.NewMockMetadataStore()
store.PromotedPathsMap = testCase.promoted
store.LogsJSONIndexes = testCase.indexes
m := NewModule(store, nil, factorytest.NewSettings())
m := NewModule(store, nil)
paths, err := m.ListPromotedPaths(ctx, testCase.filters)
require.NoError(t, err)
@@ -409,104 +408,3 @@ func TestListPromotedPaths(t *testing.T) {
})
}
}
func TestIndexMaterializedPaths(t *testing.T) {
ctx := context.Background()
bloomIndex := []telemetrytypes.TelemetryFieldKeySkipIndex{{IndexType: "bloom_filter", Granularity: 4}}
materializedKeys := []*telemetrytypes.TelemetryFieldKey{
{Name: "user.id", Signal: telemetrytypes.SignalTraces, FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString, Indexes: bloomIndex},
{Name: "tenant.id", Signal: telemetrytypes.SignalTraces, FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "retry.count", Signal: telemetrytypes.SignalTraces, FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeNumber, Indexes: []telemetrytypes.TelemetryFieldKeySkipIndex{{IndexType: "minmax", Granularity: 1}}},
{Name: "k8s.pod.name", Signal: telemetrytypes.SignalTraces, FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString, Indexes: bloomIndex},
}
userIDDDL := "`attributes.user.id_String_bloom_filter` attributes.`user.id`::String TYPE bloom_filter GRANULARITY 4"
retryCountDDL := "`attributes.retry.count_Float64_minmax` attributes.`retry.count`::Float64 TYPE minmax GRANULARITY 1"
testCases := []struct {
name string
req promotetypes.PostableIndexMaterializedPaths
keys []*telemetrytypes.TelemetryFieldKey
existing []telemetrytypes.TelemetryFieldKeySkipIndex
wantIndexed []string
wantSkipped []string
wantDDL []string
wantErr bool
}{
{
name: "DryRun_NoDDL",
req: promotetypes.PostableIndexMaterializedPaths{Signal: "traces", DryRun: true},
keys: materializedKeys,
wantIndexed: []string{"user.id", "retry.count"},
wantSkipped: []string{},
},
{
name: "Run_IndexesBaseColumn",
req: promotetypes.PostableIndexMaterializedPaths{Signal: "traces"},
keys: materializedKeys,
wantIndexed: []string{"user.id", "retry.count"},
wantSkipped: []string{},
wantDDL: []string{userIDDDL, retryCountDDL},
},
{
name: "AnyIndexBuilt_Skipped",
req: promotetypes.PostableIndexMaterializedPaths{Signal: "traces"},
keys: materializedKeys,
existing: []telemetrytypes.TelemetryFieldKeySkipIndex{{Name: "user.id", FieldContext: telemetrytypes.FieldContextAttribute, IndexType: "ngrambf_v1(4, 1024, 2, 0)"}},
wantIndexed: []string{"retry.count"},
wantSkipped: []string{"user.id"},
wantDDL: []string{retryCountDDL},
},
{
name: "UnsupportedIndexType_LoggedAndLeftOut",
req: promotetypes.PostableIndexMaterializedPaths{Signal: "traces"},
keys: append([]*telemetrytypes.TelemetryFieldKey{
{Name: "user.email", Signal: telemetrytypes.SignalTraces, FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString, Indexes: []telemetrytypes.TelemetryFieldKeySkipIndex{{IndexType: "text(tokenizer = 'splitByNonAlpha')", Granularity: 1}}},
}, materializedKeys...),
wantIndexed: []string{"user.id", "retry.count"},
wantSkipped: []string{},
wantDDL: []string{userIDDDL, retryCountDDL},
},
{
name: "LogsSignal_NoAttributeTarget_Rejected",
req: promotetypes.PostableIndexMaterializedPaths{Signal: "logs"},
wantErr: true,
},
{
name: "MissingSignal_Rejected",
wantErr: true,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ts := telemetrystoretest.New(telemetrystore.Config{}, sqlmock.QueryMatcherRegexp)
store := telemetrytypestest.NewMockMetadataStore()
store.MaterializedKeys = testCase.keys
store.LogsJSONIndexes = testCase.existing
m := NewModule(store, ts, factorytest.NewSettings())
for _, ddl := range testCase.wantDDL {
ts.Mock().ExpectExec("ADD INDEX (.+)" + regexp.QuoteMeta(ddl)).WillReturnError(nil)
}
result, err := m.IndexMaterializedPaths(ctx, testCase.req)
if testCase.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
indexed := []string{}
for _, path := range result.Indexed {
assert.False(t, path.Promote)
indexed = append(indexed, path.Path)
}
skipped := []string{}
for _, path := range result.Skipped {
skipped = append(skipped, path.Path)
}
assert.Equal(t, testCase.wantIndexed, indexed)
assert.Equal(t, testCase.wantSkipped, skipped)
assert.NoError(t, ts.Mock().ExpectationsWereMet())
})
}
}

View File

@@ -10,12 +10,9 @@ import (
type Module interface {
ListPromotedPaths(ctx context.Context, filters promotetypes.ListPromotedPathsFilters) ([]promotetypes.PromotePath, error)
PromotePaths(ctx context.Context, paths ...*promotetypes.PromotePath) error
IndexMaterializedPaths(ctx context.Context, req promotetypes.PostableIndexMaterializedPaths) (*promotetypes.IndexMaterializedPathsResult, error)
}
type Handler interface {
PromotePaths(w http.ResponseWriter, r *http.Request)
ListPromotedPaths(w http.ResponseWriter, r *http.Request)
IndexMaterializedPaths(w http.ResponseWriter, r *http.Request)
}

View File

@@ -85,18 +85,6 @@ func PromQLResources(coretypes.ExtractorContext) ([]coretypes.ResourceWithID, er
}}, nil
}
func BuilderQueryWithFilterExpressionResources(resource coretypes.Resource, bodyPath string) coretypes.ResourceExtractor {
return func(ec coretypes.ExtractorContext) ([]coretypes.ResourceWithID, error) {
return builderResources(qbtypes.QueryTypeBuilder.StringValue(), resource, gjson.GetBytes(ec.RequestBody, bodyPath).String(), nil)
}
}
func BuilderQueryWildcardResources(resource coretypes.Resource) coretypes.ResourceExtractor {
return func(coretypes.ExtractorContext) ([]coretypes.ResourceWithID, error) {
return builderResources(qbtypes.QueryTypeBuilder.StringValue(), resource, "", nil)
}
}
func resourcesForQuery(query gjson.Result, variables map[string]qbtypes.VariableItem) ([]coretypes.ResourceWithID, error) {
queryType := query.Get("type").String()
typeWildcard := queryType + "/" + coretypes.WildCardSelectorString
@@ -132,11 +120,7 @@ func resourcesForBuilderQuery(queryType string, spec gjson.Result, variables map
}
func builderQueryResourceRefs(queryType string, resource coretypes.Resource, spec gjson.Result, variables map[string]qbtypes.VariableItem) ([]coretypes.ResourceWithID, error) {
return builderResources(queryType, resource, spec.Get("filter.expression").String(), variables)
}
func builderResources(queryType string, resource coretypes.Resource, expression string, variables map[string]qbtypes.VariableItem) ([]coretypes.ResourceWithID, error) {
ids, err := builderQuerySelectors(queryType, expression, variables)
ids, err := builderQuerySelectors(queryType, spec.Get("filter.expression").String(), variables)
if err != nil {
return nil, err
}

View File

@@ -195,46 +195,3 @@ func TestTelemetrySelector(t *testing.T) {
// a value containing "/" stays one logical segment (SplitN 3).
assert.Equal(t, []string{"builder_query/signoz.workspace.key.id/a/b", "builder_query/signoz.workspace.key.id/*", "builder_query/*", "*"}, selectorValues("builder_query/signoz.workspace.key.id/a/b"))
}
func TestBuilderQueryWithFilterExpressionResources(t *testing.T) {
testCases := []struct {
name string
body string
expected []coretypes.ResourceWithID
}{
{
name: "FilterPresent_ScopedID",
body: `{"start":1,"end":2,"filter":{"expression":"signoz.workspace.key.id = 'key-a'"}}`,
expected: []coretypes.ResourceWithID{{Resource: coretypes.ResourceTelemetryResourceTraces, ID: "builder_query/signoz.workspace.key.id/key-a"}},
},
{
name: "FilterAbsent_Wildcard",
body: `{"start":1,"end":2}`,
expected: []coretypes.ResourceWithID{{Resource: coretypes.ResourceTelemetryResourceTraces, ID: "builder_query/*"}},
},
{
name: "EmptyBody_Wildcard",
body: ``,
expected: []coretypes.ResourceWithID{{Resource: coretypes.ResourceTelemetryResourceTraces, ID: "builder_query/*"}},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
refs, err := BuilderQueryWithFilterExpressionResources(coretypes.ResourceTelemetryResourceTraces, "filter.expression")(coretypes.ExtractorContext{RequestBody: []byte(testCase.body)})
require.NoError(t, err)
assert.Equal(t, testCase.expected, refs)
})
}
t.Run("InvalidExpression_Error", func(t *testing.T) {
_, err := BuilderQueryWithFilterExpressionResources(coretypes.ResourceTelemetryResourceTraces, "filter.expression")(coretypes.ExtractorContext{RequestBody: []byte(`{"filter":{"expression":"signoz.workspace.key.id = "}}`)})
require.Error(t, err)
})
}
func TestBuilderQueryWildcardResources(t *testing.T) {
refs, err := BuilderQueryWildcardResources(coretypes.ResourceTelemetryResourceTraces)(coretypes.ExtractorContext{})
require.NoError(t, err)
assert.Equal(t, []coretypes.ResourceWithID{{Resource: coretypes.ResourceTelemetryResourceTraces, ID: "builder_query/*"}}, refs)
}

View File

@@ -155,7 +155,7 @@ func NewModules(
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, fl, providerSettings, config.MetricsExplorer),
MetricReductionRule: metricReductionRule,
InfraMonitoring: implinframonitoring.NewModule(telemetryStore, telemetryMetadataStore, querier, fl, providerSettings, config.InfraMonitoring),
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore, providerSettings),
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
ServiceAccount: serviceAccount,
ServiceAccountGetter: serviceAccountGetter,
LogsPipeline: impllogspipeline.NewModule(sqlstore),

View File

@@ -38,9 +38,8 @@ var logsBodyPromotedEntry = promotetypes.NewLogsBodyTarget().Entry
var logsBodyIndexLookup = promotetypes.NewLogsBodyTarget().JSONIndexLookup()
// ClickHouse stores a `col.path::Type` skip index expression as CAST(col.path, 'Type');
// a backtick-quoted path may hold any character, parentheses included.
var simpleJSONSubColumnIndexExprRe = regexp.MustCompile("^CAST\\((?P<expr>(?:[^()`]|`[^`]*`)+), '(?P<type>.+)'\\)$")
// ClickHouse stores a `col.path::Type` skip index expression as CAST(col.path, 'Type').
var simpleJSONSubColumnIndexExprRe = regexp.MustCompile(`^CAST\((?P<expr>[^()]+), '(?P<type>.+)'\)$`)
// unfoldJSONSubColumnIndexExpr accepts both the folded (lower/assumeNotNull)
// and the bare type-cast expression forms.
@@ -199,8 +198,7 @@ func (t *telemetryMetaStore) ListJSONIndexes(ctx context.Context, lookup telemet
columnExpr, columnType, err := unfoldJSONSubColumnIndexExpr(expr)
if err != nil {
t.logger.WarnContext(ctx, "skipping JSON sub column index with unrecognized expression", slog.String("index", name), slog.String("expr", expr), errors.Attr(err))
continue
return nil, errors.WrapInternalf(err, CodeFailLoadLogsJSONIndexes, "failed to unfold JSON sub column index expression: %s", expr)
}
fdt, found := telemetrytypes.MappingJSONDataTypeToFieldDataType[columnType]

View File

@@ -118,76 +118,6 @@ func NewTelemetryMetaStore(
return t
}
// GetMaterializedKeys sets each key's Indexes to the skip indexes built on its column alone.
func (t *telemetryMetaStore) GetMaterializedKeys(ctx context.Context, signal telemetrytypes.Signal) ([]*telemetrytypes.TelemetryFieldKey, error) {
var keys []*telemetrytypes.TelemetryFieldKey
var dbName, localTableName string
var err error
switch signal {
case telemetrytypes.SignalTraces:
keys, err = t.tracesTblStatementToFieldKeys(ctx)
dbName, localTableName = t.tracesDBName, tracestelemetryschema.SpanIndexV3LocalTableName
case telemetrytypes.SignalLogs:
keys, err = t.logsTblStatementToFieldKeys(ctx)
dbName, localTableName = t.logsDBName, logstelemetryschema.LogsV2LocalTableName
default:
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "materialized keys are not supported for signal %s", signal.StringValue())
}
if err != nil {
return nil, err
}
indexes, err := t.listColumnSkipIndexes(ctx, signal, dbName, localTableName)
if err != nil {
return nil, err
}
for _, key := range keys {
for _, index := range indexes[strings.Trim(telemetrytypes.FieldKeyToMaterializedColumnName(key), "`")] {
index.Name = key.Name
index.FieldContext = key.FieldContext
index.FieldDataType = key.FieldDataType
key.Indexes = append(key.Indexes, index)
}
}
return keys, nil
}
// listColumnSkipIndexes keys each index by its expression with one pair of
// quoting backticks removed, so only an index on a single column matches a
// column name.
func (t *telemetryMetaStore) listColumnSkipIndexes(ctx context.Context, signal telemetrytypes.Signal, dbName, localTableName string) (map[string][]telemetrytypes.TelemetryFieldKeySkipIndex, error) {
ctx = withTelemetryContext(ctx, signal, "listColumnSkipIndexes")
sb := sqlbuilder.Select("name", "type_full", "expr", "granularity").From(SkipIndexTableName)
sb.Where(sb.Equal("database", dbName), sb.Equal("table", localTableName))
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to load skip indexes of %s.%s", dbName, localTableName)
}
defer rows.Close()
indexes := map[string][]telemetrytypes.TelemetryFieldKeySkipIndex{}
for rows.Next() {
var name, typeFull, expr string
var granularity uint64
if err := rows.Scan(&name, &typeFull, &expr, &granularity); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan skip index")
}
column := strings.TrimSuffix(strings.TrimPrefix(expr, "`"), "`")
indexes[column] = append(indexes[column], telemetrytypes.TelemetryFieldKeySkipIndex{
BaseColumn: column,
IndexName: name,
IndexType: typeFull,
IndexExpression: expr,
Granularity: int(granularity),
})
}
return indexes, rows.Err()
}
// tracesTblStatementToFieldKeys returns materialised attribute/resource/scope keys from the traces table.
func (t *telemetryMetaStore) tracesTblStatementToFieldKeys(ctx context.Context) ([]*telemetrytypes.TelemetryFieldKey, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{

View File

@@ -1,49 +0,0 @@
package promotetypes
import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
type IndexMaterializedPathsResult struct {
Indexed []*PromotePath `json:"indexed" required:"true"`
Skipped []*PromotePath `json:"skipped" required:"true"`
}
type PostableIndexMaterializedPaths struct {
Signal string `json:"signal" required:"true"`
DryRun bool `json:"dryRun"`
}
func (p *PostableIndexMaterializedPaths) Validate() error {
if p.Signal == "" {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "signal is required")
}
_, err := p.Target()
return err
}
func (p *PostableIndexMaterializedPaths) Target() (Target, error) {
return NewTargetFromText(p.Signal, telemetrytypes.FieldContextAttribute.StringValue())
}
// NewMaterializedPromotePath mirrors the type and granularity of the key's
// column indexes. Only the indexes are validated: the key was materialized on
// purpose, so the path checks for user input, such as cardinality, do not apply.
func NewMaterializedPromotePath(target Target, key *telemetrytypes.TelemetryFieldKey) (*PromotePath, error) {
indexes := make([]WrappedIndex, 0, len(key.Indexes))
for _, index := range key.Indexes {
indexes = append(indexes, WrappedIndex{FieldDataType: key.FieldDataType, Type: index.IndexType, Granularity: index.Granularity})
}
path := &PromotePath{
Signal: target.Entry.Signal.StringValue(),
Context: target.Entry.FieldContext.StringValue(),
Path: key.Name,
Promote: false,
Indexes: indexes,
}
if err := path.validateAndSetIndexDefaults(); err != nil {
return nil, err
}
return path, nil
}

View File

@@ -1,101 +0,0 @@
package promotetypes
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
func TestNewMaterializedPromotePath(t *testing.T) {
testCases := []struct {
name string
key *telemetrytypes.TelemetryFieldKey
wantIndexes []WrappedIndex
wantErr bool
}{
{
name: "StringPath_MirrorsColumnIndex",
key: &telemetrytypes.TelemetryFieldKey{Name: "user.id", FieldDataType: telemetrytypes.FieldDataTypeString, Indexes: []telemetrytypes.TelemetryFieldKeySkipIndex{
{IndexType: "bloom_filter", Granularity: 4},
}},
wantIndexes: []WrappedIndex{{FieldDataType: telemetrytypes.FieldDataTypeString, Type: "bloom_filter", Granularity: 4, JSONDataType: telemetrytypes.String}},
},
{
name: "StringPath_MirrorsEveryColumnIndex",
key: &telemetrytypes.TelemetryFieldKey{Name: "tenant.id", FieldDataType: telemetrytypes.FieldDataTypeString, Indexes: []telemetrytypes.TelemetryFieldKeySkipIndex{
{IndexType: "tokenbf_v1(10240, 3, 0)", Granularity: 1},
{IndexType: "bloom_filter(0.01)", Granularity: 2},
}},
wantIndexes: []WrappedIndex{
{FieldDataType: telemetrytypes.FieldDataTypeString, Type: "tokenbf_v1(10240, 3, 0)", Granularity: 1, JSONDataType: telemetrytypes.String},
{FieldDataType: telemetrytypes.FieldDataTypeString, Type: "bloom_filter(0.01)", Granularity: 2, JSONDataType: telemetrytypes.String},
},
},
{
name: "NumberPath_Float64Index",
key: &telemetrytypes.TelemetryFieldKey{Name: "retry.count", FieldDataType: telemetrytypes.FieldDataTypeNumber, Indexes: []telemetrytypes.TelemetryFieldKeySkipIndex{
{IndexType: "minmax", Granularity: 1},
}},
wantIndexes: []WrappedIndex{{FieldDataType: telemetrytypes.FieldDataTypeNumber, Type: "minmax", Granularity: 1, JSONDataType: telemetrytypes.Float64}},
},
{
name: "UnsupportedIndexType_Rejected",
key: &telemetrytypes.TelemetryFieldKey{Name: "user.email", FieldDataType: telemetrytypes.FieldDataTypeString, Indexes: []telemetrytypes.TelemetryFieldKeySkipIndex{
{IndexType: "text(tokenizer = 'splitByNonAlpha')", Granularity: 1},
}},
wantErr: true,
},
{
name: "BoolPath_Rejected",
key: &telemetrytypes.TelemetryFieldKey{Name: "user.active", FieldDataType: telemetrytypes.FieldDataTypeBool, Indexes: []telemetrytypes.TelemetryFieldKeySkipIndex{
{IndexType: "bloom_filter", Granularity: 1},
}},
wantErr: true,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
path, err := NewMaterializedPromotePath(NewTracesAttributesTarget(), testCase.key)
if testCase.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, &PromotePath{
Signal: "traces",
Context: "attribute",
Path: testCase.key.Name,
Promote: false,
Indexes: testCase.wantIndexes,
}, path)
})
}
}
func TestPostableIndexMaterializedPathsValidate(t *testing.T) {
testCases := []struct {
name string
req PostableIndexMaterializedPaths
wantErr bool
}{
{name: "TracesSignal_Valid", req: PostableIndexMaterializedPaths{Signal: "traces", DryRun: true}},
{name: "MissingSignal_Rejected", req: PostableIndexMaterializedPaths{}, wantErr: true},
{name: "UnknownSignal_Rejected", req: PostableIndexMaterializedPaths{Signal: "events"}, wantErr: true},
{name: "LogsSignal_NoAttributeTarget_Rejected", req: PostableIndexMaterializedPaths{Signal: "logs"}, wantErr: true},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
err := testCase.req.Validate()
if testCase.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}

View File

@@ -46,15 +46,6 @@ func ListPromotedPathsResources(ec coretypes.ExtractorContext) ([]coretypes.Reso
return fieldResources(signals)
}
func IndexMaterializedPathsResources(ec coretypes.ExtractorContext) ([]coretypes.ResourceWithID, error) {
signalText := gjson.GetBytes(ec.RequestBody, "signal").String()
signal, ok := telemetrytypes.SignalFromText(signalText)
if !ok {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid signal: %s", signalText)
}
return fieldResources([]telemetrytypes.Signal{signal})
}
func fieldResources(signals []telemetrytypes.Signal) ([]coretypes.ResourceWithID, error) {
resources := make([]coretypes.ResourceWithID, 0, len(signals))
seen := make(map[string]struct{})

View File

@@ -112,45 +112,3 @@ func TestListPromotedPathsResources(t *testing.T) {
})
}
}
func TestIndexMaterializedPathsResources(t *testing.T) {
testCases := []struct {
name string
body string
want []string
wantErr bool
}{
{
name: "TracesSignal_TracesField",
body: `{"signal":"traces","dryRun":true}`,
want: []string{"traces-field"},
},
{
name: "LogsSignal_LogsField",
body: `{"signal":"logs"}`,
want: []string{"logs-field"},
},
{
name: "MissingSignal_Rejected",
body: `{}`,
wantErr: true,
},
{
name: "UnsupportedSignal_Rejected",
body: `{"signal":"metrics"}`,
wantErr: true,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
resources, err := IndexMaterializedPathsResources(coretypes.ExtractorContext{RequestBody: []byte(testCase.body)})
if testCase.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, testCase.want, resourceKinds(resources))
})
}
}

View File

@@ -114,7 +114,29 @@ func (i *PromotePath) ValidateAndSetDefaults(target Target) error {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "cardinal paths can not be promoted or indexed")
}
return i.validateAndSetIndexDefaults()
for idx, index := range i.Indexes {
if index.Type == "" {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "index type is required")
}
if err := validateIndexType(index.Type); err != nil {
return err
}
if index.Granularity <= 0 {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "index granularity must be greater than 0")
}
jsonDataType, ok := telemetrytypes.MappingFieldDataTypeToJSONDataType[index.FieldDataType]
if !ok {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid column type: %s", index.FieldDataType)
}
if !jsonDataType.IndexSupported {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "index is not supported for column type: %s", index.FieldDataType)
}
i.Indexes[idx].JSONDataType = jsonDataType
}
return nil
}
func (index WrappedIndex) SkipIndexType() (schemamigrator.IndexType, error) {
@@ -200,29 +222,3 @@ func validateIndexType(indexType string) error {
}
return nil
}
func (i *PromotePath) validateAndSetIndexDefaults() error {
for idx, index := range i.Indexes {
if index.Type == "" {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "index type is required")
}
if err := validateIndexType(index.Type); err != nil {
return err
}
if index.Granularity <= 0 {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "index granularity must be greater than 0")
}
jsonDataType, ok := telemetrytypes.MappingFieldDataTypeToJSONDataType[index.FieldDataType]
if !ok {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid column type: %s", index.FieldDataType)
}
if !jsonDataType.IndexSupported {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "index is not supported for column type: %s", index.FieldDataType)
}
i.Indexes[idx].JSONDataType = jsonDataType
}
return nil
}

View File

@@ -45,9 +45,6 @@ type MetadataStore interface {
// rows templated by entry; FieldName and ReleaseTime are set per path.
PromotePaths(ctx context.Context, entry EvolutionEntry, paths ...string) error
// GetMaterializedKeys lists the keys materialized as dedicated columns.
GetMaterializedKeys(ctx context.Context, signal Signal) ([]*TelemetryFieldKey, error)
// GetFirstSeenFromMetricMetadata gets the first seen timestamp for a metric metadata lookup key.
GetFirstSeenFromMetricMetadata(ctx context.Context, lookupKeys []MetricMetadataLookupKey) (map[MetricMetadataLookupKey]int64, error)

View File

@@ -20,7 +20,6 @@ type MockMetadataStore struct {
ReducedMap map[string]bool
PromotedPathsMap map[telemetrytypes.Signal]map[string]bool
LogsJSONIndexes []telemetrytypes.TelemetryFieldKeySkipIndex
MaterializedKeys []*telemetrytypes.TelemetryFieldKey
ColumnEvolutionMetadataMap map[string][]*telemetrytypes.EvolutionEntry
LookupKeysMap map[telemetrytypes.MetricMetadataLookupKey]int64
// StaticFields holds signal-specific intrinsic field definitions (e.g. logstelemetryschema.IntrinsicFields).
@@ -377,16 +376,6 @@ func (m *MockMetadataStore) GetPromotedPaths(_ context.Context, entry telemetryt
return m.PromotedPathsMap[entry.Signal], nil
}
func (m *MockMetadataStore) GetMaterializedKeys(_ context.Context, signal telemetrytypes.Signal) ([]*telemetrytypes.TelemetryFieldKey, error) {
keys := []*telemetrytypes.TelemetryFieldKey{}
for _, key := range m.MaterializedKeys {
if key.Signal == signal {
keys = append(keys, key)
}
}
return keys, nil
}
// ListJSONIndexes narrows the stored indexes to the lookup's field context like the real query.
func (m *MockMetadataStore) ListJSONIndexes(ctx context.Context, lookup telemetrytypes.JSONIndexLookup, filters ...string) ([]telemetrytypes.TelemetryFieldKeySkipIndex, error) {
indexes := []telemetrytypes.TelemetryFieldKeySkipIndex{}

View File

@@ -32,7 +32,6 @@ pytest_plugins = [
"fixtures.alerts",
"fixtures.cloudintegrations",
"fixtures.jsontypes",
"fixtures.materialized",
"fixtures.seeder",
"fixtures.serviceaccount",
"fixtures.role",

View File

@@ -56,18 +56,12 @@ test.describe('Trace details — flamegraph', () => {
}) => {
await hoverFlamegraphSpan(page, trace.landmarks.errors[0]);
// The status row only renders in the hover card (not in waterfall rows),
// "status: error" only renders in the hover card (not in waterfall rows),
// so it proves both that the card opened and that we hovered the right
// (error) span — the bar was targeted by id via the span→rect map.
await expect(page.getByTestId('span-hover-card-status')).toHaveText(
/status\s*error/,
);
await expect(page.getByTestId('span-hover-card-start')).toHaveText(
/start\s*[\d.]+ ms/,
);
await expect(page.getByTestId('span-hover-card-duration')).toHaveText(
/duration\s*[\d.]+/,
);
await expect(page.getByText('status: error')).toBeVisible();
await expect(page.getByText(/start: [\d.]+ ms/)).toBeVisible();
await expect(page.getByText(/duration: [\d.]+/)).toBeVisible();
});
test('TC-02 clicking a bar selects the span, opens the drawer, and syncs the waterfall row', async ({

View File

@@ -1,37 +0,0 @@
from collections.abc import Callable, Generator
import pytest
from fixtures import types
# signal -> (database, local table, distributed table)
MATERIALIZED_TABLES = {
"traces": ("signoz_traces", "signoz_index_v3", "distributed_signoz_index_v3"),
}
@pytest.fixture(name="materialize_attribute", scope="function")
def materialize_attribute(signoz: types.SigNoz) -> Generator[Callable[[str, str, bool], str]]:
"""Teardown drops the columns and every index whose expression mentions a materialized key."""
cluster = signoz.telemetrystore.env["SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER"]
created: list[tuple[str, str, str]] = []
def materialize(signal: str, key: str, indexed: bool) -> str:
database, local_table, distributed_table = MATERIALIZED_TABLES[signal]
column = f"attribute_string_{key.replace('.', '$$')}"
created.append((signal, key, column))
for table in (local_table, distributed_table):
signoz.telemetrystore.conn.query(f"ALTER TABLE {database}.{table} ON CLUSTER '{cluster}' ADD COLUMN IF NOT EXISTS `{column}` LowCardinality(String) DEFAULT attributes_string['{key}']")
if indexed:
signoz.telemetrystore.conn.query(f"ALTER TABLE {database}.{local_table} ON CLUSTER '{cluster}' ADD INDEX IF NOT EXISTS `{column}_idx` `{column}` TYPE bloom_filter(0.01) GRANULARITY 64")
return column
yield materialize
for signal, key, column in created:
database, local_table, distributed_table = MATERIALIZED_TABLES[signal]
indexes = signoz.telemetrystore.conn.query(f"SELECT name FROM system.data_skipping_indices WHERE database = '{database}' AND table = '{local_table}' AND (expr LIKE '%{key}%' OR expr LIKE '%{column}%')").result_rows
for (index_name,) in indexes:
signoz.telemetrystore.conn.query(f"ALTER TABLE {database}.{local_table} ON CLUSTER '{cluster}' DROP INDEX IF EXISTS `{index_name}`")
for table in (local_table, distributed_table):
signoz.telemetrystore.conn.query(f"ALTER TABLE {database}.{table} ON CLUSTER '{cluster}' DROP COLUMN IF EXISTS `{column}`")

View File

@@ -1,154 +0,0 @@
from collections.abc import Callable
from datetime import UTC, datetime, timedelta
from http import HTTPStatus
import pytest
import requests
from wiremock.resources.mappings import Mapping
from fixtures import types
from fixtures.auth import (
USER_ADMIN_EMAIL,
USER_ADMIN_PASSWORD,
add_license,
change_user_role,
create_active_user,
)
from fixtures.fs import get_testdata_file_path
from fixtures.metrics import Metrics
from fixtures.role import transaction_group
user_password = "password123Z$"
scoped_role = "infra-scope-key-a"
scoped_email = "infra-scope-key-a@telemetry.test"
wildcard_role = "infra-wildcard"
wildcard_email = "infra-wildcard@telemetry.test"
list_paths = [
"/api/v2/infra_monitoring/hosts",
"/api/v2/infra_monitoring/pods",
"/api/v2/infra_monitoring/kube_containers",
"/api/v2/infra_monitoring/nodes",
"/api/v2/infra_monitoring/namespaces",
"/api/v2/infra_monitoring/clusters",
"/api/v2/infra_monitoring/pvcs",
"/api/v2/infra_monitoring/deployments",
"/api/v2/infra_monitoring/statefulsets",
"/api/v2/infra_monitoring/jobs",
"/api/v2/infra_monitoring/daemonsets",
]
def test_setup(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
make_http_mocks: Callable[[types.TestContainerDocker, list[Mapping]], None],
get_token: Callable[[str, str], str],
create_role: Callable[..., str],
) -> None:
add_license(signoz, make_http_mocks, get_token)
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
create_role(admin_token, scoped_role, [transaction_group("read", "telemetryresource", "metrics", ["builder_query/signoz.workspace.key.id/key-a"])])
scoped_user = create_active_user(signoz, admin_token, email=scoped_email, role="signoz-viewer", password=user_password)
change_user_role(signoz, admin_token, scoped_user, "signoz-viewer", scoped_role)
create_role(admin_token, wildcard_role, [transaction_group("read", "telemetryresource", "metrics", ["builder_query/*"])])
wildcard_user = create_active_user(signoz, admin_token, email=wildcard_email, role="signoz-viewer", password=user_password)
change_user_role(signoz, admin_token, wildcard_user, "signoz-viewer", wildcard_role)
@pytest.mark.parametrize("path", list_paths)
@pytest.mark.parametrize(
("email", "expected"),
[(scoped_email, HTTPStatus.FORBIDDEN), (wildcard_email, HTTPStatus.OK), (USER_ADMIN_EMAIL, HTTPStatus.OK)],
)
def test_list_routes(
signoz: types.SigNoz,
get_token: Callable[[str, str], str],
path: str,
email: str,
expected: HTTPStatus,
) -> None:
now = datetime.now(tz=UTC)
token = get_token(email, USER_ADMIN_PASSWORD if email == USER_ADMIN_EMAIL else user_password)
response = requests.post(
signoz.self.host_configs["8080"].get(path),
json={"start": int((now - timedelta(minutes=10)).timestamp() * 1000), "end": int(now.timestamp() * 1000), "limit": 10},
headers={"Authorization": f"Bearer {token}"},
timeout=10,
)
assert response.status_code == expected, response.text
def test_list_denied_message_names_metrics_wildcard(
signoz: types.SigNoz,
get_token: Callable[[str, str], str],
) -> None:
now = datetime.now(tz=UTC)
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v2/infra_monitoring/hosts"),
json={"start": int((now - timedelta(minutes=10)).timestamp() * 1000), "end": int(now.timestamp() * 1000), "limit": 10, "filter": {"expression": "host.name = 'web-1'"}},
headers={"Authorization": f"Bearer {get_token(scoped_email, user_password)}"},
timeout=10,
)
assert response.status_code == HTTPStatus.FORBIDDEN, response.text
assert "builder_query/*" in response.text
@pytest.mark.parametrize(
("email", "expression", "expected", "expected_hosts"),
[
(scoped_email, "signoz.workspace.key.id = 'key-a'", HTTPStatus.OK, {"acc-h1"}),
(scoped_email, "signoz.workspace.key.id = 'key-b'", HTTPStatus.FORBIDDEN, set()),
(scoped_email, "signoz.workspace.key.id IN ('key-a', 'key-b')", HTTPStatus.FORBIDDEN, set()),
(wildcard_email, None, HTTPStatus.OK, {"acc-h1", "acc-h2"}),
],
ids=["scoped_granted_key", "scoped_other_key", "scoped_both_keys", "wildcard_unfiltered"],
)
def test_hosts_scoped_read(
signoz: types.SigNoz,
get_token: Callable[[str, str], str],
insert_metrics: Callable[[list[Metrics]], None],
email: str,
expression: str | None,
expected: HTTPStatus,
expected_hosts: set[str],
) -> None:
now = datetime.now(tz=UTC).replace(microsecond=0)
insert_metrics(
[Metrics.from_dict({**metric.to_dict(), "labels": {**metric.labels, "signoz.workspace.key.id": "key-a" if metric.labels["host.name"] == "acc-h1" else "key-b"}}) for metric in Metrics.load_from_file(get_testdata_file_path("inframonitoring/hosts_value_accuracy.jsonl"), base_time=now - timedelta(minutes=4))]
)
body: dict = {"start": int((now - timedelta(minutes=5)).timestamp() * 1000), "end": int(now.timestamp() * 1000), "limit": 50}
if expression:
body["filter"] = {"expression": expression}
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v2/infra_monitoring/hosts"),
json=body,
headers={"Authorization": f"Bearer {get_token(email, user_password)}"},
timeout=10,
)
assert response.status_code == expected, response.text
if expected == HTTPStatus.OK:
assert {record["hostName"] for record in response.json()["data"]["records"]} == expected_hosts
@pytest.mark.parametrize(
("email", "expected"),
[(scoped_email, HTTPStatus.FORBIDDEN), (wildcard_email, HTTPStatus.OK), (USER_ADMIN_EMAIL, HTTPStatus.OK)],
)
def test_checks(
signoz: types.SigNoz,
get_token: Callable[[str, str], str],
email: str,
expected: HTTPStatus,
) -> None:
token = get_token(email, USER_ADMIN_PASSWORD if email == USER_ADMIN_EMAIL else user_password)
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v2/infra_monitoring/checks"),
params={"type": "hosts"},
headers={"Authorization": f"Bearer {token}"},
timeout=10,
)
assert response.status_code == expected, response.text

View File

@@ -1,62 +0,0 @@
from collections.abc import Callable
from http import HTTPStatus
import requests
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
MATERIALIZED_PATH = "/api/v1/promoted_paths/materialized"
PROBE_INDEX_EXPRS_QUERY = "SELECT expr FROM system.data_skipping_indices WHERE database = 'signoz_traces' AND table = 'signoz_index_v3' AND expr LIKE 'CAST(%materialized.%probe%'"
def test_index_materialized_paths(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
materialize_attribute: Callable[[str, str, bool], str],
) -> None:
materialize_attribute("traces", "materialized.probe", True)
materialize_attribute("traces", "materialized.unindexed_probe", False)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
resp = requests.post(
signoz.self.host_configs["8080"].get(MATERIALIZED_PATH),
json={"signal": "traces", "dryRun": True},
headers={"Authorization": f"Bearer {token}"},
timeout=30,
)
assert resp.status_code == HTTPStatus.OK, resp.text
indexed = {path["path"]: path["indexes"] for path in resp.json()["data"]["indexed"]}
assert indexed["materialized.probe"] == [{"fieldDataType": "string", "type": "bloom_filter(0.01)", "granularity": 64}]
assert "materialized.unindexed_probe" not in indexed
assert not signoz.telemetrystore.conn.query(PROBE_INDEX_EXPRS_QUERY).result_rows
resp = requests.post(
signoz.self.host_configs["8080"].get(MATERIALIZED_PATH),
json={"signal": "logs"},
headers={"Authorization": f"Bearer {token}"},
timeout=30,
)
assert resp.status_code == HTTPStatus.BAD_REQUEST, resp.text
assert not signoz.telemetrystore.conn.query(PROBE_INDEX_EXPRS_QUERY).result_rows
resp = requests.post(
signoz.self.host_configs["8080"].get(MATERIALIZED_PATH),
json={"signal": "traces"},
headers={"Authorization": f"Bearer {token}"},
timeout=30,
)
assert resp.status_code == HTTPStatus.OK, resp.text
assert "materialized.probe" in {path["path"] for path in resp.json()["data"]["indexed"]}
assert [row[0] for row in signoz.telemetrystore.conn.query(PROBE_INDEX_EXPRS_QUERY).result_rows] == ["CAST(attributes.`materialized.probe`, 'String')"]
resp = requests.post(
signoz.self.host_configs["8080"].get(MATERIALIZED_PATH),
json={"signal": "traces"},
headers={"Authorization": f"Bearer {token}"},
timeout=30,
)
assert resp.status_code == HTTPStatus.OK, resp.text
assert "materialized.probe" not in {path["path"] for path in resp.json()["data"]["indexed"]}
assert "materialized.probe" in {path["path"] for path in resp.json()["data"]["skipped"]}