mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-04 07:20:43 +00:00
Compare commits
8 Commits
nv/exec-st
...
tvats-quer
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c200693b50 | ||
|
|
0d965d63c6 | ||
|
|
ed50e4aed1 | ||
|
|
a2d281881c | ||
|
|
74d35ebafa | ||
|
|
12108b410a | ||
|
|
9d91040a55 | ||
|
|
65084ba064 |
4
.github/workflows/integrationci.yaml
vendored
4
.github/workflows/integrationci.yaml
vendored
@@ -106,8 +106,8 @@ jobs:
|
||||
- name: webdriver
|
||||
if: matrix.suite == 'callbackauthn' || matrix.suite == 'basepath'
|
||||
run: |
|
||||
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
|
||||
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee -a /etc/apt/sources.list.d/google-chrome.list
|
||||
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/google-chrome.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee -a /etc/apt/sources.list.d/google-chrome.list
|
||||
sudo apt-get update -qqy
|
||||
sudo apt-get -qqy install google-chrome-stable
|
||||
CHROME_VERSION=$(google-chrome-stable --version)
|
||||
|
||||
@@ -16,6 +16,14 @@ packages:
|
||||
filename: "maintenance.go"
|
||||
structname: 'Mock{{.InterfaceName}}'
|
||||
pkgname: '{{.SrcPackageName}}test'
|
||||
github.com/SigNoz/signoz/pkg/types/telemetrytypes:
|
||||
interfaces:
|
||||
SchemaMetadataStore:
|
||||
config:
|
||||
dir: '{{.InterfaceDir}}/telemetrytypestest'
|
||||
filename: "schema_metadata_store.go"
|
||||
structname: 'Mock{{.InterfaceName}}'
|
||||
pkgname: '{{.SrcPackageName}}test'
|
||||
github.com/SigNoz/signoz/pkg/tokenizer:
|
||||
config:
|
||||
all: true
|
||||
|
||||
@@ -178,6 +178,12 @@ querier:
|
||||
# range with padding to include slightly delayed log exports. Logs only; set
|
||||
# to 0 to disable.
|
||||
log_trace_id_window_padding: 5m
|
||||
# How far past now a query's end time may be, tolerating client clock skew.
|
||||
# Requests ending later are rejected as malformed.
|
||||
max_end_time_ahead: 24h
|
||||
schemametadatastore:
|
||||
# How long found schema metadata stays cached; 0 caches it for the life of the process.
|
||||
cache_ttl: 0s
|
||||
|
||||
##################### TelemetryStore #####################
|
||||
telemetrystore:
|
||||
|
||||
@@ -25153,9 +25153,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- rule:list
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- rule:list
|
||||
summary: List rule saved views
|
||||
tags:
|
||||
- rules
|
||||
@@ -25211,9 +25211,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- rule:list
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- rule:list
|
||||
summary: Create rule saved view
|
||||
tags:
|
||||
- rules
|
||||
@@ -25265,9 +25265,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- rule:list
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- rule:list
|
||||
summary: Delete rule saved view
|
||||
tags:
|
||||
- rules
|
||||
@@ -25334,9 +25334,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- rule:list
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- rule:list
|
||||
summary: Update rule saved view
|
||||
tags:
|
||||
- rules
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log/slog"
|
||||
"math"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
@@ -75,36 +76,54 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
|
||||
})
|
||||
// TODO(srikanthccv): parallelize this?
|
||||
p.logger.InfoContext(ctx, "fetching results for current period", slog.Any("anomaly_current_period_query", params.CurrentPeriodQuery))
|
||||
if err := p.querier.ValidateRequest(¶ms.CurrentPeriodQuery); err != nil {
|
||||
p.logger.WarnContext(ctx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
currentPeriodResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.CurrentPeriodQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.logger.InfoContext(ctx, "fetching results for past period", slog.Any("anomaly_past_period_query", params.PastPeriodQuery))
|
||||
if err := p.querier.ValidateRequest(¶ms.PastPeriodQuery); err != nil {
|
||||
p.logger.WarnContext(ctx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
pastPeriodResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.PastPeriodQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.logger.InfoContext(ctx, "fetching results for current season", slog.Any("anomaly_current_season_query", params.CurrentSeasonQuery))
|
||||
if err := p.querier.ValidateRequest(¶ms.CurrentSeasonQuery); err != nil {
|
||||
p.logger.WarnContext(ctx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
currentSeasonResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.CurrentSeasonQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.logger.InfoContext(ctx, "fetching results for past season", slog.Any("anomaly_past_season_query", params.PastSeasonQuery))
|
||||
if err := p.querier.ValidateRequest(¶ms.PastSeasonQuery); err != nil {
|
||||
p.logger.WarnContext(ctx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
pastSeasonResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.PastSeasonQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.logger.InfoContext(ctx, "fetching results for past 2 season", slog.Any("anomaly_past_2season_query", params.Past2SeasonQuery))
|
||||
if err := p.querier.ValidateRequest(¶ms.Past2SeasonQuery); err != nil {
|
||||
p.logger.WarnContext(ctx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
past2SeasonResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.Past2SeasonQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.logger.InfoContext(ctx, "fetching results for past 3 season", slog.Any("anomaly_past_3season_query", params.Past3SeasonQuery))
|
||||
if err := p.querier.ValidateRequest(¶ms.Past3SeasonQuery); err != nil {
|
||||
p.logger.WarnContext(ctx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
past3SeasonResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.Past3SeasonQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -153,6 +153,9 @@ func (module *module) GetPublicWidgetQueryRangeV2(ctx context.Context, id valuer
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := module.querier.ValidateRequest(query); err != nil {
|
||||
module.settings.Logger().WarnContext(ctx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
return module.querier.QueryRange(ctx, dashboard.OrgID, query)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ func (h *handler) QueryRange(rw http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := queryRangeRequest.Validate(); err != nil {
|
||||
if err := h.querier.ValidateRequest(&queryRangeRequest); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
"placeholder_slack_description": "Description",
|
||||
"placeholder_pager_description": "Description",
|
||||
"help_pager_client": "Shows up as event source in Pagerduty",
|
||||
"help_pager_client_url": "Shows up as event source link in Pagerduty",
|
||||
"help_pager_client_url": "Shows up as event source link in Pagerduty. Leave empty to link to this SigNoz instance",
|
||||
"help_pager_class": "The class/type of the event",
|
||||
"help_pager_details": "Specify a key-value format (must be a valid json)",
|
||||
"help_pager_group": "A cluster or grouping of sources",
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
"placeholder_opsgenie_message": "Message",
|
||||
"placeholder_opsgenie_priority": "Priority",
|
||||
"help_pager_client": "Shows up as event source in Pagerduty",
|
||||
"help_pager_client_url": "Shows up as event source link in Pagerduty",
|
||||
"help_pager_client_url": "Shows up as event source link in Pagerduty. Leave empty to link to this SigNoz instance",
|
||||
"help_pager_class": "The class/type of the event",
|
||||
"help_pager_details": "Specify a key-value format (must be a valid json)",
|
||||
"help_pager_group": "A cluster or grouping of sources",
|
||||
|
||||
@@ -85,7 +85,6 @@ export const PagerInitialConfig: Partial<AlertmanagertypesChannelPagerdutyConfig
|
||||
{{- end }}`,
|
||||
severity: '{{ (index .Alerts 0).Labels.severity }}',
|
||||
client: 'SigNoz Alert Manager',
|
||||
clientUrl: 'https://enter-signoz-host-n-port-here/alerts',
|
||||
details: {
|
||||
firing: `{{ .Alerts.Firing | toJson }}`,
|
||||
resolved: `{{ .Alerts.Resolved | toJson }}`,
|
||||
|
||||
@@ -486,4 +486,66 @@ describe('ChannelForm save', () => {
|
||||
},
|
||||
TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
"drops the previous type's templates when switching type",
|
||||
async () => {
|
||||
let body: Record<string, unknown> | undefined;
|
||||
server.use(
|
||||
rest.post(
|
||||
'http://localhost/api/v2/notification_channels',
|
||||
async (req, res, ctx) => {
|
||||
body = await req.json();
|
||||
return res(ctx.status(201), ctx.json({ status: 'success', data: {} }));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
render(<ChannelForm onDone={jest.fn()} />);
|
||||
|
||||
const user = userEvent.setup({ delay: null, pointerEventsCheck: 0 });
|
||||
await user.click(await screen.findByTestId('channel-type-select', {}, FIND));
|
||||
await user.click(await screen.findByRole('option', { name: /^Telegram$/ }));
|
||||
|
||||
expect(screen.getByTestId('telegram-title-textarea')).toHaveValue('');
|
||||
|
||||
await user.type(
|
||||
screen.getByTestId('channel-name-textbox'),
|
||||
'telegram alerts',
|
||||
);
|
||||
await user.type(screen.getByTestId('telegram-bot-token-textbox'), '123:abc');
|
||||
await user.type(screen.getByTestId('telegram-chat-id-textbox'), '-100123');
|
||||
|
||||
const save = screen.getByTestId('save-channel-button');
|
||||
await waitFor(
|
||||
() => expect(save).toHaveAttribute('aria-disabled', 'false'),
|
||||
FIND,
|
||||
);
|
||||
await user.click(save);
|
||||
|
||||
await waitFor(() => expect(body).toBeDefined(), FIND);
|
||||
expect(
|
||||
(body as { config: { spec: Record<string, unknown> } }).config.spec,
|
||||
).toStrictEqual({
|
||||
botToken: '123:abc',
|
||||
chatId: -100123,
|
||||
sendResolved: true,
|
||||
});
|
||||
},
|
||||
TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
'leaves the pagerduty client url empty so the backend links to this instance',
|
||||
async () => {
|
||||
render(<ChannelForm onDone={jest.fn()} />);
|
||||
|
||||
const user = userEvent.setup({ delay: null, pointerEventsCheck: 0 });
|
||||
await user.click(await screen.findByTestId('channel-type-select', {}, FIND));
|
||||
await user.click(await screen.findByRole('option', { name: /^Pagerduty$/ }));
|
||||
|
||||
expect(screen.getByTestId('pager-client-url-textarea')).toHaveValue('');
|
||||
},
|
||||
TEST_TIMEOUT,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -86,9 +86,25 @@ export function useChannelFormState({
|
||||
|
||||
setTypeState(nextType);
|
||||
|
||||
const nextDefaults = ChannelInitialConfig[nextType];
|
||||
|
||||
// the previous type's templates would otherwise be sent as the new
|
||||
// type's values; sendResolved is a general setting and is kept.
|
||||
// '' not undefined: a field mounting on undefined falls back to the
|
||||
// draft's defaults, and blanks are dropped on save anyway
|
||||
Object.keys(ChannelInitialConfig[type])
|
||||
.filter((field) => field !== 'sendResolved' && !(field in nextDefaults))
|
||||
.forEach((field) => {
|
||||
form.setValue(
|
||||
field as keyof ChannelFormValues,
|
||||
'' as ChannelFormValues[keyof ChannelFormValues],
|
||||
{ shouldDirty: true },
|
||||
);
|
||||
});
|
||||
|
||||
// shouldDirty, because the type lives outside the form: without it
|
||||
// picking a different type leaves the form pristine and unsavable.
|
||||
Object.entries(ChannelInitialConfig[nextType]).forEach(([field, value]) => {
|
||||
Object.entries(nextDefaults).forEach(([field, value]) => {
|
||||
form.setValue(
|
||||
field as keyof ChannelFormValues,
|
||||
value as ChannelFormValues[keyof ChannelFormValues],
|
||||
|
||||
@@ -164,12 +164,12 @@ export const TILE_DRAWINGS = {
|
||||
<path d="M4 20 C 20 20, 22 12, 32 12 S 44 4, 60 4" />
|
||||
</TileSvg>
|
||||
),
|
||||
barsSideBySide: (
|
||||
barsOverlap: (
|
||||
<TileSvg>
|
||||
<rect x={10} y={10} width={7} height={12} {...FILLED} opacity={0.5} />
|
||||
<rect x={18} y={4} width={7} height={18} {...FILLED} />
|
||||
<rect x={36} y={12} width={7} height={10} {...FILLED} opacity={0.5} />
|
||||
<rect x={44} y={7} width={7} height={15} {...FILLED} />
|
||||
<rect x={14} y={6} width={11} height={16} {...FILLED} opacity={0.5} />
|
||||
<rect x={14} y={12} width={11} height={10} {...FILLED} />
|
||||
<rect x={38} y={10} width={11} height={12} {...FILLED} opacity={0.5} />
|
||||
<rect x={38} y={14} width={11} height={8} {...FILLED} />
|
||||
</TileSvg>
|
||||
),
|
||||
barsStacked: (
|
||||
|
||||
@@ -56,7 +56,7 @@ function VisualizationSection({
|
||||
value?.timePreference ?? DashboardtypesTimePreferenceDTO.global_time;
|
||||
const barLayout = value?.stackedBarChart
|
||||
? BarLayout.STACKED
|
||||
: BarLayout.SIDE_BY_SIDE;
|
||||
: BarLayout.OVERLAP;
|
||||
const stack = value?.stack ?? defaultValue?.stack;
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,15 +4,15 @@ import type { ConfigTileItem } from '../../controls/ConfigTiles/ConfigTiles';
|
||||
import { TILE_DRAWINGS } from '../../controls/drawings/tileDrawings';
|
||||
|
||||
export enum BarLayout {
|
||||
SIDE_BY_SIDE = 'side',
|
||||
OVERLAP = 'overlap',
|
||||
STACKED = 'stacked',
|
||||
}
|
||||
|
||||
export const BAR_LAYOUT_OPTIONS: ConfigTileItem<BarLayout>[] = [
|
||||
{
|
||||
value: BarLayout.SIDE_BY_SIDE,
|
||||
label: 'Side by side',
|
||||
drawing: TILE_DRAWINGS.barsSideBySide,
|
||||
value: BarLayout.OVERLAP,
|
||||
label: 'Overlap',
|
||||
drawing: TILE_DRAWINGS.barsOverlap,
|
||||
},
|
||||
{
|
||||
value: BarLayout.STACKED,
|
||||
@@ -22,8 +22,7 @@ export const BAR_LAYOUT_OPTIONS: ConfigTileItem<BarLayout>[] = [
|
||||
];
|
||||
|
||||
export const BAR_LAYOUT_HELP: Record<BarLayout, string> = {
|
||||
[BarLayout.SIDE_BY_SIDE]:
|
||||
'Each series gets its own bar, side by side, for easy comparison.',
|
||||
[BarLayout.OVERLAP]: 'Each series is drawn from zero and may hide others.',
|
||||
[BarLayout.STACKED]:
|
||||
'Bars sit on top of each other, so the height shows the total.',
|
||||
};
|
||||
|
||||
@@ -149,61 +149,97 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rule_views", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.ListRuleViews), handler.OpenAPIDef{
|
||||
ID: "ListRuleViews",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "List rule saved views",
|
||||
Description: "Returns every saved view in the calling user's org. Saved views are shared org-wide.",
|
||||
Response: new(ruletypes.ListableRuleViews),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/rule_views", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.rulerHandler.ListRuleViews, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ListRuleViews",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "List rule saved views",
|
||||
Description: "Returns every saved view in the calling user's org. Saved views are shared org-wide.",
|
||||
Response: new(ruletypes.ListableRuleViews),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceRule.Scope(coretypes.VerbList)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceRule,
|
||||
Verb: coretypes.VerbList,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}),
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rule_views", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.CreateRuleView), handler.OpenAPIDef{
|
||||
ID: "CreateRuleView",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Create rule saved view",
|
||||
Description: "Persists the calling user's rule listing state (query, states, sort, order) as a named, reusable view shared across the org.",
|
||||
Request: new(ruletypes.PostableRuleView),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(ruletypes.RuleView),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/rule_views", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.rulerHandler.CreateRuleView, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateRuleView",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Create rule saved view",
|
||||
Description: "Persists the calling user's rule listing state (query, states, sort, order) as a named, reusable view shared across the org.",
|
||||
Request: new(ruletypes.PostableRuleView),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(ruletypes.RuleView),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceRule.Scope(coretypes.VerbList)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceRule,
|
||||
Verb: coretypes.VerbList,
|
||||
Category: coretypes.ActionCategoryConfigurationChange,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rule_views/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.UpdateRuleView), handler.OpenAPIDef{
|
||||
ID: "UpdateRuleView",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Update rule saved view",
|
||||
Description: "Replaces a saved view's name and data. Saved views are shared org-wide.",
|
||||
Request: new(ruletypes.UpdatableRuleView),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(ruletypes.RuleView),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/rule_views/{id}", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.rulerHandler.UpdateRuleView, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateRuleView",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Update rule saved view",
|
||||
Description: "Replaces a saved view's name and data. Saved views are shared org-wide.",
|
||||
Request: new(ruletypes.UpdatableRuleView),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(ruletypes.RuleView),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceRule.Scope(coretypes.VerbList)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceRule,
|
||||
Verb: coretypes.VerbList,
|
||||
Category: coretypes.ActionCategoryConfigurationChange,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rule_views/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.DeleteRuleView), handler.OpenAPIDef{
|
||||
ID: "DeleteRuleView",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Delete rule saved view",
|
||||
Description: "Removes a saved view. Saved views are shared org-wide. Deleting a non-existent view returns 404.",
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
if err := router.Handle("/api/v2/rule_views/{id}", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.rulerHandler.DeleteRuleView, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteRuleView",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Delete rule saved view",
|
||||
Description: "Removes a saved view. Saved views are shared org-wide. Deleting a non-existent view returns 404.",
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceRule.Scope(coretypes.VerbList)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceRule,
|
||||
Verb: coretypes.VerbList,
|
||||
Category: coretypes.ActionCategoryConfigurationChange,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}),
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -191,6 +192,9 @@ func (m *module) getTopClusterGroupsAndMetadata(
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
if err := m.querier.ValidateRequest(topReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
resp, err := m.querier.QueryRange(gCtx, orgID, topReq)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/clickhousesql"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetryschema/metricstelemetryschema"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
@@ -232,6 +233,9 @@ func (m *module) getTopContainerGroupsAndMetadata(
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
if err := m.querier.ValidateRequest(topReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
resp, err := m.querier.QueryRange(gCtx, orgID, topReq)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -182,6 +183,9 @@ func (m *module) getTopDaemonSetGroupsAndMetadata(
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
if err := m.querier.ValidateRequest(topReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
resp, err := m.querier.QueryRange(gCtx, orgID, topReq)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -174,6 +175,9 @@ func (m *module) getTopDeploymentGroupsAndMetadata(
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
if err := m.querier.ValidateRequest(topReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
resp, err := m.querier.QueryRange(gCtx, orgID, topReq)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/clickhousesql"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetryschema/metricstelemetryschema"
|
||||
@@ -321,6 +322,9 @@ func (m *module) getTopHostGroupsAndMetadata(
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
if err := m.querier.ValidateRequest(topReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
resp, err := m.querier.QueryRange(gCtx, orgID, topReq)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -182,6 +183,9 @@ func (m *module) getTopJobGroupsAndMetadata(
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
if err := m.querier.ValidateRequest(topReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
resp, err := m.querier.QueryRange(gCtx, orgID, topReq)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
@@ -222,6 +223,9 @@ func (m *module) ListHosts(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
if err := m.querier.ValidateRequest(fullQueryReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
@@ -325,6 +329,9 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
if err := m.querier.ValidateRequest(fullQueryReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
@@ -436,6 +443,9 @@ func (m *module) ListContainers(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
if err := m.querier.ValidateRequest(fullQueryReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
@@ -552,6 +562,9 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
if err := m.querier.ValidateRequest(fullQueryReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
@@ -666,6 +679,9 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
if err := m.querier.ValidateRequest(fullQueryReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
@@ -778,6 +794,9 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
if err := m.querier.ValidateRequest(fullQueryReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
@@ -877,6 +896,9 @@ func (m *module) ListVolumes(ctx context.Context, orgID valuer.UUID, req *infram
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newVolumesTableListQuery())
|
||||
if err := m.querier.ValidateRequest(fullQueryReq); err != nil {
|
||||
m.logger.WarnContext(ctx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
queryResp, err := m.querier.QueryRange(ctx, orgID, fullQueryReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -974,6 +996,9 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
if err := m.querier.ValidateRequest(fullQueryReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
@@ -1083,6 +1108,9 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
if err := m.querier.ValidateRequest(fullQueryReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
@@ -1192,6 +1220,9 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
if err := m.querier.ValidateRequest(fullQueryReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
@@ -1301,6 +1332,9 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
if err := m.querier.ValidateRequest(fullQueryReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
|
||||
return err
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -157,6 +158,9 @@ func (m *module) getTopNamespaceGroupsAndMetadata(
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
if err := m.querier.ValidateRequest(topReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
resp, err := m.querier.QueryRange(gCtx, orgID, topReq)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/clickhousesql"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetryschema/metricstelemetryschema"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
@@ -199,6 +200,9 @@ func (m *module) getTopNodeGroupsAndMetadata(
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
if err := m.querier.ValidateRequest(topReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
resp, err := m.querier.QueryRange(gCtx, orgID, topReq)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/clickhousesql"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetryschema/metricstelemetryschema"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
@@ -239,6 +240,9 @@ func (m *module) getTopPodGroupsAndMetadata(
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
if err := m.querier.ValidateRequest(topReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
resp, err := m.querier.QueryRange(gCtx, orgID, topReq)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -174,6 +175,9 @@ func (m *module) getTopStatefulSetGroupsAndMetadata(
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
if err := m.querier.ValidateRequest(topReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
resp, err := m.querier.QueryRange(gCtx, orgID, topReq)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -133,6 +134,9 @@ func (m *module) getTopVolumeGroupsAndMetadata(
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
if err := m.querier.ValidateRequest(topReq); err != nil {
|
||||
m.logger.WarnContext(gCtx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
resp, err := m.querier.QueryRange(gCtx, orgID, topReq)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
@@ -24,10 +26,12 @@ const unmappedModelsLookback = time.Hour
|
||||
type module struct {
|
||||
store llmpricingruletypes.Store
|
||||
querier querier.Querier
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewModule(store llmpricingruletypes.Store, querier querier.Querier) llmpricingrule.Module {
|
||||
return &module{store: store, querier: querier}
|
||||
func NewModule(store llmpricingruletypes.Store, querier querier.Querier, settings factory.ProviderSettings) llmpricingrule.Module {
|
||||
logger := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/modules/llmpricingrule/impllmpricingrule").Logger()
|
||||
return &module{store: store, querier: querier, logger: logger}
|
||||
}
|
||||
|
||||
func (module *module) List(ctx context.Context, orgID valuer.UUID, offset, limit int, search string, isOverride *bool) ([]*llmpricingruletypes.LLMPricingRule, int, error) {
|
||||
@@ -201,6 +205,9 @@ func (module *module) discoverModels(ctx context.Context, orgID valuer.UUID) ([]
|
||||
},
|
||||
}
|
||||
|
||||
if err := module.querier.ValidateRequest(req); err != nil {
|
||||
module.logger.WarnContext(ctx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
resp, err := module.querier.QueryRange(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -538,7 +538,7 @@ func TestTracesNumberIndexExpr_MatchesQuerierFilters(t *testing.T) {
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
conditions, _, err := querybuilder.Conditions(context.Background(), queryInfo, tracestelemetryschema.NewStorage(), &key, testCase.operator, testCase.value, map[string][]*telemetrytypes.TelemetryFieldKey{key.Name: {&key}}, false, sb)
|
||||
conditions, _, err := querybuilder.Conditions(context.Background(), queryInfo, tracestelemetryschema.NewStorage(nil), &key, testCase.operator, testCase.value, map[string][]*telemetrytypes.TelemetryFieldKey{key.Name: {&key}}, false, sb)
|
||||
require.NoError(t, err)
|
||||
sb.Where(conditions...)
|
||||
sql, _ := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
@@ -2,8 +2,10 @@ package implrawdataexport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
@@ -14,11 +16,13 @@ import (
|
||||
|
||||
type Module struct {
|
||||
querier querier.Querier
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewModule(querier querier.Querier) rawdataexport.Module {
|
||||
func NewModule(querier querier.Querier, settings factory.ProviderSettings) rawdataexport.Module {
|
||||
return &Module{
|
||||
querier: querier,
|
||||
logger: factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/modules/rawdataexport/implrawdataexport").Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +32,10 @@ func (m *Module) ExportRawData(ctx context.Context, orgID valuer.UUID, rangeRequ
|
||||
instrumentationtypes.CodeFunctionName: "ExportRawData",
|
||||
})
|
||||
|
||||
if err := m.querier.ValidateRequest(rangeRequest, qbtypes.WithSkipLimitOffsetValidation()); err != nil {
|
||||
m.logger.WarnContext(ctx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
|
||||
traceOperatorQueryIndex := rangeRequest.TraceOperatorQueryIndex()
|
||||
|
||||
queries := rangeRequest.CompositeQuery.Queries
|
||||
|
||||
@@ -3,12 +3,14 @@ package implservices
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"strconv"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/services"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/telemetryschema/tracestelemetryschema"
|
||||
@@ -24,13 +26,15 @@ import (
|
||||
type module struct {
|
||||
Querier querier.Querier
|
||||
TelemetryStore telemetrystore.TelemetryStore
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewModule constructs the services module with the provided querier dependency.
|
||||
func NewModule(q querier.Querier, ts telemetrystore.TelemetryStore) services.Module {
|
||||
func NewModule(q querier.Querier, ts telemetrystore.TelemetryStore, settings factory.ProviderSettings) services.Module {
|
||||
return &module{
|
||||
Querier: q,
|
||||
TelemetryStore: ts,
|
||||
logger: factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/modules/services/implservices").Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +224,9 @@ func (m *module) buildQueryRangeRequest(req *servicetypesv1.Request) (*qbtypes.Q
|
||||
|
||||
// executeQuery calls the underlying Querier with the provided request.
|
||||
func (m *module) executeQuery(ctx context.Context, orgUUID valuer.UUID, qr *qbtypes.QueryRangeRequest) (*qbtypes.QueryRangeResponse, error) {
|
||||
if err := m.Querier.ValidateRequest(qr); err != nil {
|
||||
m.logger.WarnContext(ctx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
return m.Querier.QueryRange(ctx, orgUUID, qr)
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ func (m *module) GetSpanPercentile(ctx context.Context, orgID valuer.UUID, req *
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := queryRangeRequest.Validate(); err != nil {
|
||||
if err := m.querier.ValidateRequest(queryRangeRequest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -64,11 +64,11 @@ type traceStore struct {
|
||||
flagger flagger.Flagger
|
||||
}
|
||||
|
||||
func NewTraceStore(ts telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, fl flagger.Flagger) *traceStore {
|
||||
func NewTraceStore(ts telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, schemaMetadataStore telemetrytypes.SchemaMetadataStore, fl flagger.Flagger) *traceStore {
|
||||
return &traceStore{
|
||||
telemetryStore: ts,
|
||||
metadataStore: metadataStore,
|
||||
storage: tracestelemetryschema.NewStorage(),
|
||||
storage: tracestelemetryschema.NewStorage(schemaMetadataStore),
|
||||
flagger: fl,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func newTestStore(matcher sqlmock.QueryMatcher) *spantypestest.TraceStoreTest {
|
||||
|
||||
func newTestStoreWithMetadata(matcher sqlmock.QueryMatcher, metadataStore telemetrytypes.MetadataStore, fl flagger.Flagger) *spantypestest.TraceStoreTest {
|
||||
ts := telemetrystoretest.New(telemetrystore.Config{}, matcher)
|
||||
return spantypestest.New(impltracedetail.NewTraceStore(ts, metadataStore, fl), ts.Mock())
|
||||
return spantypestest.New(impltracedetail.NewTraceStore(ts, metadataStore, nil, fl), ts.Mock())
|
||||
}
|
||||
|
||||
func TestGetTraceBounds(t *testing.T) {
|
||||
|
||||
@@ -59,7 +59,7 @@ func (handler *handler) QueryRange(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
// Validate the query request
|
||||
if err := queryRangeRequest.Validate(); err != nil {
|
||||
if err := handler.querier.ValidateRequest(&queryRangeRequest); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
@@ -140,7 +140,9 @@ func (handler *handler) QueryRawStream(rw http.ResponseWriter, req *http.Request
|
||||
}
|
||||
// create the v5 request param
|
||||
queryRangeRequest := qbtypes.QueryRangeRequest{
|
||||
Start: start,
|
||||
// Coerce the window to epoch milliseconds up front (End may be 0 for the
|
||||
// open-ended stream, which ToMilliSecs leaves untouched).
|
||||
Start: qbtypes.ToMilliSecs(start),
|
||||
RequestType: qbtypes.RequestTypeRawStream,
|
||||
CompositeQuery: qbtypes.CompositeQuery{
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
@@ -186,7 +188,7 @@ func (handler *handler) QueryRawStream(rw http.ResponseWriter, req *http.Request
|
||||
}
|
||||
|
||||
// Validate the query request
|
||||
if err := queryRangeRequest.Validate(); err != nil {
|
||||
if err := handler.querier.ValidateRequest(&queryRangeRequest); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/statementbuilder"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
)
|
||||
|
||||
const DefaultMaxConcurrentQueries = 8
|
||||
@@ -20,6 +21,8 @@ type Config struct {
|
||||
MaxConcurrentQueries int `yaml:"max_concurrent_queries" mapstructure:"max_concurrent_queries"`
|
||||
// LogTraceIDWindowPadding is the padding added to narrowed down timerange from trace summary to logs with trace_id filter.
|
||||
LogTraceIDWindowPadding time.Duration `yaml:"log_trace_id_window_padding" mapstructure:"log_trace_id_window_padding"`
|
||||
// MaxEndTimeAhead is how far past now a query's end time may be; it tolerates client clock skew.
|
||||
MaxEndTimeAhead time.Duration `yaml:"max_end_time_ahead" mapstructure:"max_end_time_ahead"`
|
||||
|
||||
// Keys sit under querier.skip_resource_fingerprint.
|
||||
statementbuilder.Config `mapstructure:",squash" yaml:",squash"`
|
||||
@@ -37,6 +40,7 @@ func newConfig() factory.Config {
|
||||
FluxInterval: 5 * time.Minute,
|
||||
MaxConcurrentQueries: DefaultMaxConcurrentQueries,
|
||||
LogTraceIDWindowPadding: 5 * time.Minute,
|
||||
MaxEndTimeAhead: qbtypes.DefaultMaxEndTimeAhead,
|
||||
Config: statementbuilder.NewConfig(),
|
||||
}
|
||||
}
|
||||
@@ -55,6 +59,9 @@ func (c Config) Validate() error {
|
||||
if c.LogTraceIDWindowPadding < 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "log_trace_id_window_padding must not be negative, got %v", c.LogTraceIDWindowPadding)
|
||||
}
|
||||
if c.MaxEndTimeAhead <= 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "max_end_time_ahead must be positive, got %v", c.MaxEndTimeAhead)
|
||||
}
|
||||
// Embedded Validate is shadowed by this one; call it explicitly.
|
||||
if err := c.Config.Validate(); err != nil {
|
||||
return err
|
||||
|
||||
46
pkg/querier/config_test.go
Normal file
46
pkg/querier/config_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package querier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/config"
|
||||
"github.com/SigNoz/signoz/pkg/config/configtest"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
func TestSchemaMetadataStoreCacheTTLFromEnv(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
want time.Duration
|
||||
}{
|
||||
{
|
||||
name: "Unset_CachesForProcessLifetime",
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "Nanosecond_ExpiresImmediately",
|
||||
env: map[string]string{"SIGNOZ_QUERIER_SCHEMAMETADATASTORE_CACHE__TTL": "1ns"},
|
||||
want: time.Nanosecond,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
for key, value := range testCase.env {
|
||||
t.Setenv(key, value)
|
||||
}
|
||||
conf, err := config.New(context.Background(), configtest.NewResolverConfig(), []factory.ConfigFactory{NewConfigFactory()})
|
||||
require.NoError(t, err)
|
||||
|
||||
actual := Config{}
|
||||
require.NoError(t, conf.Unmarshal("querier", &actual))
|
||||
assert.Equal(t, testCase.want, actual.SchemaMetadataStore.CacheTTL)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,12 @@ func makeBuckets(start, end uint64) []tsRange {
|
||||
startNS := querybuilder.ToNanoSecs(start)
|
||||
endNS := querybuilder.ToNanoSecs(end)
|
||||
|
||||
// A cursor that reaches the window boundary can leave end <= start; the
|
||||
// subtractions below are unsigned and would wrap.
|
||||
if endNS <= startNS {
|
||||
return nil
|
||||
}
|
||||
|
||||
if endNS-startNS <= uint64(hourNanos) {
|
||||
return []tsRange{{fromNS: startNS, toNS: endNS}}
|
||||
}
|
||||
@@ -20,9 +26,9 @@ func makeBuckets(start, end uint64) []tsRange {
|
||||
curEnd := endNS
|
||||
|
||||
for {
|
||||
curStart := curEnd - bucket
|
||||
if curStart < startNS {
|
||||
curStart = startNS
|
||||
curStart := startNS
|
||||
if curEnd-startNS > bucket {
|
||||
curStart = curEnd - bucket
|
||||
}
|
||||
out = append(out, tsRange{fromNS: curStart, toNS: curEnd})
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ func (q *querier) QueryRangePreview(
|
||||
opts qbtypes.QueryRangePreviewOptions,
|
||||
) (*qbtypes.QueryRangePreviewResponse, error) {
|
||||
|
||||
validationOpts, err := req.ValidateRequestScope()
|
||||
validationOpts, err := req.ValidateRequestScope(q.validationOptions()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ type Querier interface {
|
||||
statsreporter.StatsCollector
|
||||
// QueryRangePreview validates and renders the queries without executing them.
|
||||
QueryRangePreview(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest, opts qbtypes.QueryRangePreviewOptions) (*qbtypes.QueryRangePreviewResponse, error)
|
||||
// ValidateRequest checks req against the querier's preconditions; opts add to the querier's own.
|
||||
ValidateRequest(req *qbtypes.QueryRangeRequest, opts ...qbtypes.ValidationOption) error
|
||||
}
|
||||
|
||||
type querier struct {
|
||||
@@ -62,6 +64,7 @@ type querier struct {
|
||||
liveDataRefresh time.Duration
|
||||
builderConfig builderConfig
|
||||
maxConcurrentQueries int
|
||||
config Config
|
||||
}
|
||||
|
||||
var _ Querier = (*querier)(nil)
|
||||
@@ -80,12 +83,14 @@ func New(
|
||||
traceOperatorStmtBuilder qbtypes.TraceOperatorStatementBuilder,
|
||||
bucketCache BucketCache,
|
||||
flagger flagger.Flagger,
|
||||
logTraceIDWindowPadding time.Duration,
|
||||
maxConcurrentQueries int,
|
||||
config Config,
|
||||
) *querier {
|
||||
querierSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/querier")
|
||||
if maxConcurrentQueries <= 0 {
|
||||
maxConcurrentQueries = DefaultMaxConcurrentQueries
|
||||
if config.MaxConcurrentQueries <= 0 {
|
||||
config.MaxConcurrentQueries = DefaultMaxConcurrentQueries
|
||||
}
|
||||
if config.MaxEndTimeAhead <= 0 {
|
||||
config.MaxEndTimeAhead = qbtypes.DefaultMaxEndTimeAhead
|
||||
}
|
||||
return &querier{
|
||||
logger: querierSettings.Logger(),
|
||||
@@ -103,19 +108,32 @@ func New(
|
||||
bucketCache: bucketCache,
|
||||
liveDataRefresh: 5 * time.Second,
|
||||
builderConfig: builderConfig{
|
||||
logTraceIDWindowPaddingMS: uint64(logTraceIDWindowPadding.Milliseconds()),
|
||||
logTraceIDWindowPaddingMS: uint64(config.LogTraceIDWindowPadding.Milliseconds()),
|
||||
},
|
||||
maxConcurrentQueries: maxConcurrentQueries,
|
||||
maxConcurrentQueries: config.MaxConcurrentQueries,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (q *querier) ValidateRequest(req *qbtypes.QueryRangeRequest, opts ...qbtypes.ValidationOption) error {
|
||||
return req.Validate(append(opts, q.validationOptions()...)...)
|
||||
}
|
||||
|
||||
func (q *querier) validationOptions() []qbtypes.ValidationOption {
|
||||
return []qbtypes.ValidationOption{qbtypes.WithMaxEndTimeAhead(q.config.MaxEndTimeAhead)}
|
||||
}
|
||||
|
||||
func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest) (*qbtypes.QueryRangeResponse, error) {
|
||||
|
||||
// Coerce the window to epoch milliseconds up front so every downstream
|
||||
// consumer (TimeRange, narrowWindowByTraceID, step interval, etc.) can
|
||||
// safely assume ms regardless of the resolution the caller sent.
|
||||
req.Start = querybuilder.ToMilliSecs(req.Start)
|
||||
req.End = querybuilder.ToMilliSecs(req.End)
|
||||
// Callers that build the request in-process never pass through
|
||||
// UnmarshalJSON, so normalise here too.
|
||||
req.Start = qbtypes.ToMilliSecs(req.Start)
|
||||
req.End = qbtypes.ToMilliSecs(req.End)
|
||||
|
||||
// In-process callers skip ValidateRequest; the time window still guards the querier.
|
||||
if err := req.ValidateTimeRange(q.validationOptions()...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
event := &qbtypes.QBEvent{
|
||||
Version: "v5",
|
||||
@@ -534,10 +552,8 @@ func familyTemporality(temporalities map[string]metrictypes.Temporality, names [
|
||||
|
||||
func (q *querier) QueryRawStream(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest, client *qbtypes.RawStream) {
|
||||
|
||||
// Coerce the window to epoch milliseconds up front (End may be 0 for the
|
||||
// open-ended stream, which ToMilliSecs leaves untouched).
|
||||
req.Start = querybuilder.ToMilliSecs(req.Start)
|
||||
req.End = querybuilder.ToMilliSecs(req.End)
|
||||
req.Start = qbtypes.ToMilliSecs(req.Start)
|
||||
req.End = qbtypes.ToMilliSecs(req.End)
|
||||
|
||||
event := &qbtypes.QBEvent{
|
||||
Version: "v5",
|
||||
|
||||
@@ -57,8 +57,7 @@ func TestQueryRange_MetricTypeMissing(t *testing.T) {
|
||||
nil, // traceOperatorStmtBuilder
|
||||
nil, // bucketCache
|
||||
flaggertest.New(t), // flagger
|
||||
0, // logTraceIDWindowPadding
|
||||
0, // maxConcurrentQueries
|
||||
Config{},
|
||||
)
|
||||
|
||||
req := &qbtypes.QueryRangeRequest{
|
||||
@@ -130,8 +129,7 @@ func TestQueryRange_MetricTypeFromStore(t *testing.T) {
|
||||
nil, // traceOperatorStmtBuilder
|
||||
nil, // bucketCache
|
||||
flaggertest.New(t), // flagger
|
||||
0, // logTraceIDWindowPadding
|
||||
0, // maxConcurrentQueries
|
||||
Config{},
|
||||
)
|
||||
|
||||
req := &qbtypes.QueryRangeRequest{
|
||||
|
||||
@@ -37,6 +37,10 @@ func (m *MockQuerier) QueryRawStream(ctx context.Context, orgID valuer.UUID, req
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockQuerier) ValidateRequest(req *qbtypes.QueryRangeRequest, opts ...qbtypes.ValidationOption) error {
|
||||
return req.Validate(opts...)
|
||||
}
|
||||
|
||||
func (m *MockQuerier) QueryRangePreview(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest, opts qbtypes.QueryRangePreviewOptions) (*qbtypes.QueryRangePreviewResponse, error) {
|
||||
if m.QueryRangePreviewFunc != nil {
|
||||
return m.QueryRangePreviewFunc(ctx, orgID, req, opts)
|
||||
|
||||
@@ -50,8 +50,7 @@ func NewFactory(
|
||||
traceOperatorStmtBuilder,
|
||||
bucketCache,
|
||||
flagger,
|
||||
cfg.LogTraceIDWindowPadding,
|
||||
cfg.MaxConcurrentQueries,
|
||||
cfg,
|
||||
), nil
|
||||
},
|
||||
)
|
||||
|
||||
@@ -3718,6 +3718,9 @@ func (aH *APIHandler) getDomainList(w http.ResponseWriter, r *http.Request) {
|
||||
instrumentationtypes.CodeFunctionName: "getDomainList",
|
||||
})
|
||||
// Execute the query using the v5 querier
|
||||
if err := aH.Signoz.Querier.ValidateRequest(queryRangeRequest); err != nil {
|
||||
aH.logger.WarnContext(ctx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
result, err := aH.Signoz.Querier.QueryRange(ctx, orgID, queryRangeRequest)
|
||||
if err != nil {
|
||||
aH.logger.ErrorContext(r.Context(), "query execution failed", errors.Attr(err))
|
||||
@@ -3778,6 +3781,9 @@ func (aH *APIHandler) getDomainInfo(w http.ResponseWriter, r *http.Request) {
|
||||
instrumentationtypes.CodeFunctionName: "getDomainInfo",
|
||||
})
|
||||
// Execute the query using the v5 querier
|
||||
if err := aH.Signoz.Querier.ValidateRequest(queryRangeRequest); err != nil {
|
||||
aH.logger.WarnContext(ctx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
result, err := aH.Signoz.Querier.QueryRange(ctx, orgID, queryRangeRequest)
|
||||
if err != nil {
|
||||
aH.logger.ErrorContext(r.Context(), "query execution failed", errors.Attr(err))
|
||||
|
||||
@@ -114,13 +114,14 @@ func NewTestManager(t *testing.T, testOpts *TestManagerOptions) *Manager {
|
||||
|
||||
// Create querier with test values
|
||||
metadataStore := telemetrymetadata.NewTelemetryMetaStore(providerSettings, telemetryStore, flagger)
|
||||
schemaMetadataStore := telemetrymetadata.NewSchemaMetadataStore(telemetryStore, 0)
|
||||
cfg := statementbuilder.Config{}
|
||||
ctx := context.Background()
|
||||
traceStmtBuilder, err := tracesstatementbuilder.NewFactory(telemetryStore, metadataStore, flagger).New(ctx, providerSettings, cfg)
|
||||
traceStmtBuilder, err := tracesstatementbuilder.NewFactory(telemetryStore, metadataStore, schemaMetadataStore, flagger).New(ctx, providerSettings, cfg)
|
||||
require.NoError(t, err)
|
||||
aiTraceStmtBuilder, err := aistatementbuilder.NewFactory(telemetryStore, metadataStore, flagger).New(ctx, providerSettings, cfg)
|
||||
aiTraceStmtBuilder, err := aistatementbuilder.NewFactory(telemetryStore, metadataStore, schemaMetadataStore, flagger).New(ctx, providerSettings, cfg)
|
||||
require.NoError(t, err)
|
||||
traceOperatorStmtBuilder, err := tracesstatementbuilder.NewOperatorFactory(telemetryStore, metadataStore, flagger).New(ctx, providerSettings, cfg)
|
||||
traceOperatorStmtBuilder, err := tracesstatementbuilder.NewOperatorFactory(telemetryStore, metadataStore, schemaMetadataStore, flagger).New(ctx, providerSettings, cfg)
|
||||
require.NoError(t, err)
|
||||
logStmtBuilder, err := logsstatementbuilder.NewFactory(telemetryStore, metadataStore, flagger).New(ctx, providerSettings, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -50,8 +50,7 @@ func prepareQuerierForMetrics(t *testing.T, telemetryStore telemetrystore.Teleme
|
||||
nil, // traceOperatorStmtBuilder
|
||||
nil, // bucketCache
|
||||
fl,
|
||||
0,
|
||||
0, // maxConcurrentQueries (0 means default)
|
||||
querier.Config{},
|
||||
), metadataStore
|
||||
}
|
||||
|
||||
@@ -85,8 +84,7 @@ func prepareQuerierForLogs(t *testing.T, telemetryStore telemetrystore.Telemetry
|
||||
nil, // traceOperatorStmtBuilder
|
||||
nil, // bucketCache
|
||||
fl,
|
||||
5*time.Minute, // logTraceIDWindowPadding
|
||||
0, // maxConcurrentQueries (0 means default)
|
||||
querier.Config{LogTraceIDWindowPadding: 5 * time.Minute},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -104,7 +102,7 @@ func prepareQuerierForTraces(t *testing.T, telemetryStore telemetrystore.Telemet
|
||||
metadataStore.KeysMap = keysMap
|
||||
|
||||
fl := flaggertest.New(t)
|
||||
traceStmtBuilder, err := tracesstatementbuilder.NewFactory(telemetryStore, metadataStore, fl).New(context.Background(), providerSettings, statementbuilder.Config{})
|
||||
traceStmtBuilder, err := tracesstatementbuilder.NewFactory(telemetryStore, metadataStore, nil, fl).New(context.Background(), providerSettings, statementbuilder.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
return querier.New(
|
||||
@@ -121,8 +119,7 @@ func prepareQuerierForTraces(t *testing.T, telemetryStore telemetrystore.Telemet
|
||||
nil, // traceOperatorStmtBuilder
|
||||
nil, // bucketCache
|
||||
fl,
|
||||
0,
|
||||
0, // maxConcurrentQueries (0 means default)
|
||||
querier.Config{},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -140,7 +137,7 @@ func prepareQuerierForAITraces(t *testing.T, telemetryStore telemetrystore.Telem
|
||||
metadataStore.KeysMap = keysMap
|
||||
|
||||
fl := flaggertest.New(t)
|
||||
aiTraceStmtBuilder, err := aistatementbuilder.NewFactory(telemetryStore, metadataStore, fl).New(context.Background(), providerSettings, statementbuilder.Config{})
|
||||
aiTraceStmtBuilder, err := aistatementbuilder.NewFactory(telemetryStore, metadataStore, nil, fl).New(context.Background(), providerSettings, statementbuilder.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
return querier.New(
|
||||
@@ -157,7 +154,6 @@ func prepareQuerierForAITraces(t *testing.T, telemetryStore telemetrystore.Telem
|
||||
nil, // traceOperatorStmtBuilder
|
||||
nil, // bucketCache
|
||||
fl,
|
||||
0,
|
||||
0, // maxConcurrentQueries (0 means default)
|
||||
querier.Config{},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -138,6 +138,9 @@ func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID,
|
||||
instrumentationtypes.CodeFunctionName: "buildAndRunQuery",
|
||||
})
|
||||
|
||||
if err := r.querier.ValidateRequest(params); err != nil {
|
||||
r.logger.WarnContext(ctx, "invalid query range request", errors.Attr(err))
|
||||
}
|
||||
v5Result, err := r.querier.QueryRange(ctx, orgID, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -33,28 +33,6 @@ func ToNanoSecs(epoch uint64) uint64 {
|
||||
return temp * uint64(math.Pow(10, float64(19-count)))
|
||||
}
|
||||
|
||||
// ToMilliSecs takes an epoch whose resolution is inferred from its magnitude
|
||||
// (s/ms/µs/ns) and returns it in milliseconds. A millisecond epoch for the
|
||||
// current era has 13 digits (e.g. ~1.7e12 in 2026), so the value is scaled so
|
||||
// its digit-width matches: smaller values (seconds) are scaled up, larger ones
|
||||
// (micro/nanoseconds) are scaled down. Zero is returned unchanged.
|
||||
func ToMilliSecs(epoch uint64) uint64 {
|
||||
if epoch == 0 {
|
||||
return 0
|
||||
}
|
||||
temp := epoch
|
||||
count := 0
|
||||
for epoch != 0 {
|
||||
epoch /= 10
|
||||
count++
|
||||
}
|
||||
const msDigits = 13
|
||||
if count < msDigits {
|
||||
return temp * uint64(math.Pow(10, float64(msDigits-count)))
|
||||
}
|
||||
return temp / uint64(math.Pow(10, float64(count-msDigits)))
|
||||
}
|
||||
|
||||
// TODO(srikanthccv): should these be rounded to nearest multiple of 60 instead of 5 if step > 60?
|
||||
// That would make graph look nice but "nice" but should be less important than the usefulness.
|
||||
func RecommendedStepInterval(start, end uint64) uint64 {
|
||||
|
||||
@@ -60,51 +60,3 @@ func TestToNanoSecs(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToMilliSecs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
epoch uint64
|
||||
expected uint64
|
||||
}{
|
||||
{
|
||||
name: "10-digit Unix timestamp (seconds) - 2023-01-01 00:00:00 UTC",
|
||||
epoch: 1672531200, // seconds
|
||||
expected: 1672531200000, // * 10^3
|
||||
},
|
||||
{
|
||||
name: "13-digit Unix timestamp (milliseconds) - already ms",
|
||||
epoch: 1672531200000,
|
||||
expected: 1672531200000, // unchanged
|
||||
},
|
||||
{
|
||||
name: "16-digit Unix timestamp (microseconds)",
|
||||
epoch: 1672531200000000, // microseconds
|
||||
expected: 1672531200000, // / 10^3
|
||||
},
|
||||
{
|
||||
name: "19-digit Unix timestamp (nanoseconds)",
|
||||
epoch: 1672531200000000000, // nanoseconds
|
||||
expected: 1672531200000, // / 10^6
|
||||
},
|
||||
{
|
||||
name: "Unix epoch start - zero is unchanged",
|
||||
epoch: 0,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "Recent timestamp in seconds - 2024-05-25 12:00:00 UTC",
|
||||
epoch: 1716638400,
|
||||
expected: 1716638400000,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ToMilliSecs(tt.epoch)
|
||||
if result != tt.expected {
|
||||
t.Errorf("ToMilliSecs(%d) = %d, want %d", tt.epoch, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func TestNewHandlers(t *testing.T) {
|
||||
spanMapperRegistry, err := implspanmapper.NewSystemGroupRegistry()
|
||||
require.NoError(t, err)
|
||||
spanMapperModule := implspanmapper.NewModule(implspanmapper.NewStore(sqlstore), spanMapperRegistry, providerSettings)
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, nil, nil, nil, retentionGetter, flagger, tagModule, nil, spanMapperModule)
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, nil, nil, nil, retentionGetter, flagger, tagModule, nil, spanMapperModule)
|
||||
|
||||
querierHandler := querier.NewHandler(providerSettings, nil, nil)
|
||||
registryHandler := factory.NewHandler(nil)
|
||||
|
||||
@@ -108,6 +108,7 @@ func NewModules(
|
||||
querier querier.Querier,
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
telemetryMetadataStore telemetrytypes.MetadataStore,
|
||||
schemaMetadataStore telemetrytypes.SchemaMetadataStore,
|
||||
authNs map[authtypes.AuthNProvider]authn.AuthN,
|
||||
authz authz.AuthZ,
|
||||
cache cache.Cache,
|
||||
@@ -147,11 +148,11 @@ func NewModules(
|
||||
RetentionGetter: retentionGetter,
|
||||
QuickFilter: quickfilter,
|
||||
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
|
||||
RawDataExport: implrawdataexport.NewModule(querier),
|
||||
RawDataExport: implrawdataexport.NewModule(querier, providerSettings),
|
||||
AuthDomain: authDomainModule,
|
||||
Session: implsession.NewModule(providerSettings, authNs, userSetter, userGetter, authDomainModule, tokenizer, orgGetter, authz, config.Global),
|
||||
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
|
||||
Services: implservices.NewModule(querier, telemetryStore),
|
||||
Services: implservices.NewModule(querier, telemetryStore, providerSettings),
|
||||
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, fl, providerSettings, config.MetricsExplorer),
|
||||
MetricReductionRule: metricReductionRule,
|
||||
InfraMonitoring: implinframonitoring.NewModule(telemetryStore, telemetryMetadataStore, querier, fl, providerSettings, config.InfraMonitoring),
|
||||
@@ -161,9 +162,9 @@ func NewModules(
|
||||
LogsPipeline: impllogspipeline.NewModule(sqlstore),
|
||||
RuleStateHistory: implrulestatehistory.NewModule(implrulestatehistory.NewStore(telemetryStore, telemetryMetadataStore, providerSettings.Logger), ruleStore, providerSettings.Logger),
|
||||
CloudIntegration: cloudIntegrationModule,
|
||||
TraceDetail: impltracedetail.NewModule(impltracedetail.NewTraceStore(telemetryStore, telemetryMetadataStore, fl), providerSettings, config.TraceDetail),
|
||||
TraceDetail: impltracedetail.NewModule(impltracedetail.NewTraceStore(telemetryStore, telemetryMetadataStore, schemaMetadataStore, fl), providerSettings, config.TraceDetail),
|
||||
SpanMapper: spanMapper,
|
||||
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore), querier),
|
||||
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore), querier, providerSettings),
|
||||
Tag: tagModule,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestNewModules(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
spanMapperModule := implspanmapper.NewModule(implspanmapper.NewStore(sqlstore), spanMapperRegistry, providerSettings)
|
||||
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, serviceAccount, serviceAccountGetter, implcloudintegration.NewModule(), retentionGetter, flagger, tagModule, implmetricreductionrule.NewModule(), spanMapperModule)
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore, serviceAccount, serviceAccountGetter, implcloudintegration.NewModule(), retentionGetter, flagger, tagModule, implmetricreductionrule.NewModule(), spanMapperModule)
|
||||
|
||||
reflectVal := reflect.ValueOf(modules)
|
||||
for i := 0; i < reflectVal.NumField(); i++ {
|
||||
|
||||
@@ -270,6 +270,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddRoutePolicyTuplesFactory(sqlstore),
|
||||
sqlmigration.NewRenameSavedViewDeprecatedTraceFieldsFactory(),
|
||||
sqlmigration.NewAddDowntimeTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddRuleTuplesFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ type SigNoz struct {
|
||||
}
|
||||
|
||||
// newQueryStack assembles the query stack once and returns, in order: the shared
|
||||
// telemetry metadata store (reused elsewhere in signoz.New), the per-signal
|
||||
// telemetry metadata and schema metadata stores (reused elsewhere in signoz.New), the per-signal
|
||||
// statement builders (trace, ai-trace, log, audit, metric, meter, trace-operator),
|
||||
// and the bucket cache. It is the only place that imports the concrete
|
||||
// statement-builder sub-packages.
|
||||
@@ -115,6 +115,7 @@ func newQueryStack(
|
||||
fl flagger.Flagger,
|
||||
) (
|
||||
telemetrytypes.MetadataStore,
|
||||
telemetrytypes.SchemaMetadataStore,
|
||||
qbtypes.StatementBuilder[qbtypes.TraceAggregation],
|
||||
qbtypes.StatementBuilder[qbtypes.TraceAggregation],
|
||||
qbtypes.StatementBuilder[qbtypes.LogAggregation],
|
||||
@@ -128,38 +129,39 @@ func newQueryStack(
|
||||
metadataStore := telemetrymetadata.NewTelemetryMetaStore(settings, telemetryStore, fl)
|
||||
|
||||
cfg := config.Querier.Config
|
||||
traceStmtBuilder, err := tracesstatementbuilder.NewFactory(telemetryStore, metadataStore, fl).New(ctx, settings, cfg)
|
||||
schemaMetadataStore := telemetrymetadata.NewSchemaMetadataStore(telemetryStore, cfg.SchemaMetadataStore.CacheTTL)
|
||||
traceStmtBuilder, err := tracesstatementbuilder.NewFactory(telemetryStore, metadataStore, schemaMetadataStore, fl).New(ctx, settings, cfg)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil, nil, err
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
aiTraceStmtBuilder, err := aistatementbuilder.NewFactory(telemetryStore, metadataStore, fl).New(ctx, settings, cfg)
|
||||
aiTraceStmtBuilder, err := aistatementbuilder.NewFactory(telemetryStore, metadataStore, schemaMetadataStore, fl).New(ctx, settings, cfg)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil, nil, err
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
traceOperatorStmtBuilder, err := tracesstatementbuilder.NewOperatorFactory(telemetryStore, metadataStore, fl).New(ctx, settings, cfg)
|
||||
traceOperatorStmtBuilder, err := tracesstatementbuilder.NewOperatorFactory(telemetryStore, metadataStore, schemaMetadataStore, fl).New(ctx, settings, cfg)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil, nil, err
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
logStmtBuilder, err := logsstatementbuilder.NewFactory(telemetryStore, metadataStore, fl).New(ctx, settings, cfg)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil, nil, err
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
auditStmtBuilder, err := auditstatementbuilder.NewFactory(metadataStore, fl).New(ctx, settings, cfg)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil, nil, err
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
metricStmtBuilder, err := metricsstatementbuilder.NewFactory(metadataStore, fl).New(ctx, settings, cfg)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil, nil, err
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
meterStmtBuilder, err := meterstatementbuilder.NewFactory(metadataStore, fl).New(ctx, settings, cfg)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil, nil, err
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
bucketCache := querier.NewBucketCache(settings, cache, config.Querier.CacheTTL, config.Querier.FluxInterval)
|
||||
|
||||
return metadataStore, traceStmtBuilder, aiTraceStmtBuilder, logStmtBuilder, auditStmtBuilder, metricStmtBuilder, meterStmtBuilder, traceOperatorStmtBuilder, bucketCache, nil
|
||||
return metadataStore, schemaMetadataStore, traceStmtBuilder, aiTraceStmtBuilder, logStmtBuilder, auditStmtBuilder, metricStmtBuilder, meterStmtBuilder, traceOperatorStmtBuilder, bucketCache, nil
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -323,7 +325,7 @@ func New(
|
||||
|
||||
// Assemble the query stack (metadata store, statement builders, bucket cache) once,
|
||||
// and reuse the single metadata store everywhere downstream.
|
||||
telemetryMetadataStore, traceStmtBuilder, aiTraceStmtBuilder, logStmtBuilder, auditStmtBuilder, metricStmtBuilder, meterStmtBuilder, traceOperatorStmtBuilder, bucketCache, err := newQueryStack(ctx, providerSettings, config, telemetrystore, cache, flagger)
|
||||
telemetryMetadataStore, schemaMetadataStore, traceStmtBuilder, aiTraceStmtBuilder, logStmtBuilder, auditStmtBuilder, metricStmtBuilder, meterStmtBuilder, traceOperatorStmtBuilder, bucketCache, err := newQueryStack(ctx, providerSettings, config, telemetrystore, cache, flagger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -535,7 +537,7 @@ func New(
|
||||
}
|
||||
spanMapperModule := implspanmapper.NewModule(implspanmapper.NewStore(sqlstore), spanMapperRegistry, providerSettings)
|
||||
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, userRoleStore, serviceAccount, serviceAccountGetter, cloudIntegrationModule, retentionGetter, flagger, tagModule, metricReductionRuleModule, spanMapperModule)
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, schemaMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, userRoleStore, serviceAccount, serviceAccountGetter, cloudIntegrationModule, retentionGetter, flagger, tagModule, metricReductionRuleModule, spanMapperModule)
|
||||
|
||||
// Initialize ruler from the variant-specific provider factories
|
||||
rulerInstance, err := factory.NewProviderFromNamedMap(ctx, providerSettings, config.Ruler, rulerProviderFactories(cache, alertmanager, sqlstore, telemetrystore, telemetryMetadataStore, prometheus, orgGetter, modules.RuleStateHistory, querier, queryParser), "signoz")
|
||||
|
||||
140
pkg/sqlmigration/143_add_rule_tuples.go
Normal file
140
pkg/sqlmigration/143_add_rule_tuples.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addRuleTuples struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewAddRuleTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_rule_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addRuleTuples{sqlstore: sqlstore}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addRuleTuples) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addRuleTuples) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
var storeID string
|
||||
err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var orgIDs []string
|
||||
err = tx.NewSelect().
|
||||
Table("organizations").
|
||||
Column("id").
|
||||
Scan(ctx, &orgIDs)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
||||
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
|
||||
|
||||
crudVerbs := []string{"create", "read", "update", "delete", "list"}
|
||||
readVerbs := []string{"read", "list"}
|
||||
|
||||
tuples := make([]migrationTuple, 0, 12)
|
||||
for _, verb := range crudVerbs {
|
||||
tuples = append(tuples,
|
||||
migrationTuple{authtypes.SigNozAdminRoleName, "metaresource", "rule", verb},
|
||||
migrationTuple{authtypes.SigNozEditorRoleName, "metaresource", "rule", verb},
|
||||
)
|
||||
}
|
||||
for _, verb := range readVerbs {
|
||||
tuples = append(tuples, migrationTuple{authtypes.SigNozViewerRoleName, "metaresource", "rule", verb})
|
||||
}
|
||||
|
||||
for _, orgID := range orgIDs {
|
||||
for _, tuple := range tuples {
|
||||
entropy := ulid.DefaultEntropy()
|
||||
now := time.Now().UTC()
|
||||
tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String()
|
||||
|
||||
objectID := "organization/" + orgID + "/" + tuple.objectName + "/*"
|
||||
roleSubject := "organization/" + orgID + "/role/" + tuple.roleName
|
||||
|
||||
if isPG {
|
||||
user := "role:" + roleSubject + "#assignee"
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, user, "userset", tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
continue
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, user, 0, tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", "userset", tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
continue
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", 0, tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *addRuleTuples) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -17,9 +17,10 @@ import (
|
||||
func NewFactory(
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
metadataStore telemetrytypes.MetadataStore,
|
||||
schemaMetadataStore telemetrytypes.SchemaMetadataStore,
|
||||
fl flagger.Flagger,
|
||||
) factory.ProviderFactory[qbtypes.StatementBuilder[qbtypes.TraceAggregation], statementbuilder.Config] {
|
||||
return scopedtraces.NewFactory(factory.MustNewName("ai"), Scope(), telemetryStore, metadataStore, fl)
|
||||
return scopedtraces.NewFactory(factory.MustNewName("ai"), Scope(), telemetryStore, metadataStore, schemaMetadataStore, fl)
|
||||
}
|
||||
|
||||
// Scope describes gen_ai for the scoped trace builder: an AI trace has >=1 gen_ai
|
||||
|
||||
@@ -107,7 +107,7 @@ func newTestBuilderWithKeys(t *testing.T, keysMap map[string][]*telemetrytypes.T
|
||||
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
mockMetadataStore.KeysMap = keysMap
|
||||
|
||||
stmtBuilder, err := NewFactory(nil, mockMetadataStore, flaggertest.New(t)).
|
||||
stmtBuilder, err := NewFactory(nil, mockMetadataStore, nil, flaggertest.New(t)).
|
||||
New(context.Background(), settings, statementbuilder.Config{SkipResourceFingerprint: statementbuilder.SkipResourceFingerprint{Threshold: 100000}})
|
||||
require.NoError(t, err)
|
||||
return stmtBuilder
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package statementbuilder
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -11,6 +13,12 @@ type SkipResourceFingerprint struct {
|
||||
Threshold uint64 `yaml:"threshold" mapstructure:"threshold"`
|
||||
}
|
||||
|
||||
// SchemaMetadataStore configures the schema metadata store.
|
||||
type SchemaMetadataStore struct {
|
||||
// CacheTTL is how long found entries stay cached; 0 caches them for the life of the process.
|
||||
CacheTTL time.Duration `yaml:"cache_ttl" mapstructure:"cache_ttl"`
|
||||
}
|
||||
|
||||
// Config represents the configuration for the statement builders.
|
||||
type Config struct {
|
||||
// SkipResourceFingerprint configures when the resource fingerprint subquery is skipped in favor of main-table filtering.
|
||||
@@ -21,6 +29,8 @@ type Config struct {
|
||||
// SearchMaxScanRowsJSONBody is the same budget for body_v2, where each row costs far
|
||||
// more: toString() rebuilds every document and no skip index prunes (0 disables).
|
||||
SearchMaxScanRowsJSONBody int64 `yaml:"search_max_scan_rows_json_body" mapstructure:"search_max_scan_rows_json_body"`
|
||||
// SchemaMetadataStore configures the schema metadata store.
|
||||
SchemaMetadataStore SchemaMetadataStore `yaml:"schemametadatastore" mapstructure:"schemametadatastore"`
|
||||
}
|
||||
|
||||
func NewConfig() Config {
|
||||
@@ -45,5 +55,8 @@ func (c Config) Validate() error {
|
||||
if c.SearchMaxScanRowsJSONBody < 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "search_max_scan_rows_json_body must not be negative, got %v", c.SearchMaxScanRowsJSONBody)
|
||||
}
|
||||
if c.SchemaMetadataStore.CacheTTL < 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "schemametadatastore.cache_ttl must not be negative, got %v", c.SchemaMetadataStore.CacheTTL)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -50,16 +50,17 @@ func NewFactory(
|
||||
scope TraceScope,
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
metadataStore telemetrytypes.MetadataStore,
|
||||
schemaMetadataStore telemetrytypes.SchemaMetadataStore,
|
||||
fl flagger.Flagger,
|
||||
) factory.ProviderFactory[qbtypes.StatementBuilder[qbtypes.TraceAggregation], statementbuilder.Config] {
|
||||
return factory.NewProviderFactory(
|
||||
name,
|
||||
func(ctx context.Context, settings factory.ProviderSettings, cfg statementbuilder.Config) (qbtypes.StatementBuilder[qbtypes.TraceAggregation], error) {
|
||||
traceStmtBuilder, err := tracesstatementbuilder.NewFactory(telemetryStore, metadataStore, fl).New(ctx, settings, cfg)
|
||||
traceStmtBuilder, err := tracesstatementbuilder.NewFactory(telemetryStore, metadataStore, schemaMetadataStore, fl).New(ctx, settings, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewScopedTraceStatementBuilder(settings, metadataStore, tracestelemetryschema.NewStorage(), scope, traceStmtBuilder, fl), nil
|
||||
return NewScopedTraceStatementBuilder(settings, metadataStore, tracestelemetryschema.NewStorage(schemaMetadataStore), scope, traceStmtBuilder, fl), nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -26,7 +27,9 @@ var jsonAttrColRe = regexp.MustCompile(`,\s*attributes\s*(,| FROM )`)
|
||||
func newBulkTestBuilder(t *testing.T, releaseTime time.Time) *traceQueryStatementBuilder {
|
||||
t.Helper()
|
||||
fl := flaggertest.WithBooleanFlags(t, map[string]bool{flagger.FeatureUseTraceAttributesJSON.String(): true})
|
||||
storage := tracestelemetryschema.NewStorage()
|
||||
schemaMetadataStore := telemetrytypestest.NewMockSchemaMetadataStore(t)
|
||||
schemaMetadataStore.EXPECT().GetBaseColumnEvolutions(mock.Anything, telemetrytypes.SignalTraces, telemetrytypes.FieldContextAttribute).Return(tracestelemetryschema.MockAttributeEvolutionData(releaseTime), nil).Maybe()
|
||||
storage := tracestelemetryschema.NewStorage(schemaMetadataStore)
|
||||
store := telemetrytypestest.NewMockMetadataStore()
|
||||
store.KeysMap = tracestelemetryschema.BuildCompleteFieldKeyMap(releaseTime)
|
||||
store.KeysMap["http.route"] = []*telemetrytypes.TelemetryFieldKey{{
|
||||
@@ -148,3 +151,153 @@ func TestGroupByAttributeHomeAcrossRollout(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownAttributeFollowsRollout(t *testing.T) {
|
||||
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
||||
rel := releaseTime.UnixMilli()
|
||||
day := int64(24 * time.Hour / time.Millisecond)
|
||||
|
||||
b := newBulkTestBuilder(t, releaseTime)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
startMs uint64
|
||||
endMs uint64
|
||||
wantContains []string
|
||||
wantNotContains []string
|
||||
}{
|
||||
{
|
||||
name: "BeforeRollout_ReadsLegacyMap",
|
||||
startMs: uint64(rel - 2*day),
|
||||
endMs: uint64(rel - day),
|
||||
wantContains: []string{"attributes_string['unknown.attr'] = ?", "mapContains(attributes_string, 'unknown.attr')"},
|
||||
wantNotContains: []string{"attributes.`unknown.attr`"},
|
||||
},
|
||||
{
|
||||
name: "StraddlingRollout_JSONThenLegacyFallback",
|
||||
startMs: uint64(rel - day),
|
||||
endMs: uint64(rel + day),
|
||||
wantContains: []string{"attributes.`unknown.attr` IS NOT NULL", "attributes.`unknown.attr`::String", "attributes_string['unknown.attr']"},
|
||||
wantNotContains: nil,
|
||||
},
|
||||
{
|
||||
name: "AfterRollout_ReadsJSONOnly",
|
||||
startMs: uint64(rel + day),
|
||||
endMs: uint64(rel + 2*day),
|
||||
wantContains: []string{"attributes.`unknown.attr`::String = ?"},
|
||||
wantNotContains: []string{"attributes_string"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
stmt, err := b.Build(
|
||||
context.Background(), valuer.UUID{}, testCase.startMs, testCase.endMs,
|
||||
qbtypes.RequestTypeTimeSeries,
|
||||
qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
||||
Aggregations: []qbtypes.TraceAggregation{{Expression: "count()"}},
|
||||
Filter: &qbtypes.Filter{Expression: "unknown.attr = 'x'"},
|
||||
GroupBy: []qbtypes.GroupByKey{{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "unknown.attr",
|
||||
}}},
|
||||
Limit: 10,
|
||||
},
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
for _, want := range testCase.wantContains {
|
||||
assert.Contains(t, stmt.Query, want)
|
||||
}
|
||||
for _, unwanted := range testCase.wantNotContains {
|
||||
assert.NotContains(t, stmt.Query, unwanted)
|
||||
}
|
||||
assert.Contains(t, strings.Join(stmt.Warnings, "\n"), "not found in metadata")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownAttributeFollowsRolloutAfterRelease(t *testing.T) {
|
||||
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
||||
startMs := uint64(releaseTime.Add(24 * time.Hour).UnixMilli())
|
||||
endMs := uint64(releaseTime.Add(48 * time.Hour).UnixMilli())
|
||||
|
||||
b := newBulkTestBuilder(t, releaseTime)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
requestType qbtypes.RequestType
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]
|
||||
wantContains []string
|
||||
wantNotContains []string
|
||||
}{
|
||||
{
|
||||
name: "AggregationArgument_ReadsJSONOnly",
|
||||
requestType: qbtypes.RequestTypeTimeSeries,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
||||
Aggregations: []qbtypes.TraceAggregation{{Expression: "sum(unknown.num)"}},
|
||||
},
|
||||
wantContains: []string{"attributes.`unknown.num`"},
|
||||
wantNotContains: []string{"attributes_number['unknown.num']"},
|
||||
},
|
||||
{
|
||||
name: "RawSelectAndOrder_ReadsJSONOnly",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
SelectFields: []telemetrytypes.TelemetryFieldKey{{Name: "unknown.attr"}},
|
||||
Order: []qbtypes.OrderBy{{Key: qbtypes.OrderByKey{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "unknown.attr"}}, Direction: qbtypes.OrderDirectionAsc}},
|
||||
Limit: 10,
|
||||
},
|
||||
wantContains: []string{"attributes.`unknown.attr`"},
|
||||
wantNotContains: []string{"attributes_string['unknown.attr']"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
stmt, err := b.Build(context.Background(), valuer.UUID{}, startMs, endMs, testCase.requestType, testCase.query, nil)
|
||||
require.NoError(t, err)
|
||||
for _, want := range testCase.wantContains {
|
||||
assert.Contains(t, stmt.Query, want)
|
||||
}
|
||||
for _, unwanted := range testCase.wantNotContains {
|
||||
assert.NotContains(t, stmt.Query, unwanted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceOperatorUnknownAttributeFollowsRollout(t *testing.T) {
|
||||
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
||||
startMs := uint64(releaseTime.Add(24 * time.Hour).UnixMilli())
|
||||
endMs := uint64(releaseTime.Add(48 * time.Hour).UnixMilli())
|
||||
|
||||
fl := flaggertest.WithBooleanFlags(t, map[string]bool{flagger.FeatureUseTraceAttributesJSON.String(): true})
|
||||
schemaMetadataStore := telemetrytypestest.NewMockSchemaMetadataStore(t)
|
||||
schemaMetadataStore.EXPECT().GetBaseColumnEvolutions(mock.Anything, telemetrytypes.SignalTraces, telemetrytypes.FieldContextAttribute).Return(tracestelemetryschema.MockAttributeEvolutionData(releaseTime), nil).Maybe()
|
||||
storage := tracestelemetryschema.NewStorage(schemaMetadataStore)
|
||||
store := telemetrytypestest.NewMockMetadataStore()
|
||||
store.KeysMap = tracestelemetryschema.BuildCompleteFieldKeyMap(releaseTime)
|
||||
store.ColumnEvolutionMetadataMap["traces:attribute:__all__"] = tracestelemetryschema.MockAttributeEvolutionData(releaseTime)
|
||||
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, storage, fl, telemetrytypes.SignalTraces)
|
||||
traceStmtBuilder := NewTraceQueryStatementBuilder(instrumentationtest.New().ToProviderSettings(), store, storage, aggExprRewriter, nil, fl, false, 100000)
|
||||
b := NewTraceOperatorStatementBuilder(instrumentationtest.New().ToProviderSettings(), store, storage, traceStmtBuilder, aggExprRewriter, fl)
|
||||
|
||||
operator := qbtypes.QueryBuilderTraceOperator{Expression: "A => B", Limit: 10}
|
||||
require.NoError(t, operator.ParseExpression())
|
||||
compositeQuery := &qbtypes.CompositeQuery{Queries: []qbtypes.QueryEnvelope{
|
||||
{Type: qbtypes.QueryTypeBuilder, Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{Name: "A", Signal: telemetrytypes.SignalTraces, Filter: &qbtypes.Filter{Expression: "unknown.attr = 'x'"}}},
|
||||
{Type: qbtypes.QueryTypeBuilder, Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{Name: "B", Signal: telemetrytypes.SignalTraces, Filter: &qbtypes.Filter{Expression: "unknown.other = 'y'"}}},
|
||||
}}
|
||||
|
||||
stmt, err := b.Build(context.Background(), valuer.UUID{}, startMs, endMs, qbtypes.RequestTypeRaw, operator, compositeQuery)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, stmt.Query, "attributes.`unknown.attr`::String = ?")
|
||||
assert.Contains(t, stmt.Query, "attributes.`unknown.other`::String = ?")
|
||||
assert.NotContains(t, stmt.Query, "attributes_string['unknown.attr']")
|
||||
assert.NotContains(t, stmt.Query, "attributes_string['unknown.other']")
|
||||
}
|
||||
|
||||
@@ -49,12 +49,13 @@ var _ qbtypes.StatementBuilder[qbtypes.TraceAggregation] = (*traceQueryStatement
|
||||
func NewFactory(
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
metadataStore telemetrytypes.MetadataStore,
|
||||
schemaMetadataStore telemetrytypes.SchemaMetadataStore,
|
||||
fl flagger.Flagger,
|
||||
) factory.ProviderFactory[qbtypes.StatementBuilder[qbtypes.TraceAggregation], statementbuilder.Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("traces"),
|
||||
func(_ context.Context, settings factory.ProviderSettings, cfg statementbuilder.Config) (qbtypes.StatementBuilder[qbtypes.TraceAggregation], error) {
|
||||
storage := tracestelemetryschema.NewStorage()
|
||||
storage := tracestelemetryschema.NewStorage(schemaMetadataStore)
|
||||
aggExprRewriter := querybuilder.NewAggExprRewriter(settings, nil, storage, fl, telemetrytypes.SignalTraces)
|
||||
return NewTraceQueryStatementBuilder(
|
||||
settings, metadataStore, storage, aggExprRewriter, telemetryStore, fl,
|
||||
|
||||
@@ -465,7 +465,7 @@ func TestStatementBuilder(t *testing.T) {
|
||||
}
|
||||
|
||||
fl := flaggertest.New(t)
|
||||
storage := tracestelemetryschema.NewStorage()
|
||||
storage := tracestelemetryschema.NewStorage(nil)
|
||||
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
mockMetadataStore.KeysMap = tracestelemetryschema.BuildCompleteFieldKeyMap(releaseTime)
|
||||
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, storage, fl, telemetrytypes.SignalTraces)
|
||||
@@ -764,7 +764,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
}
|
||||
|
||||
fl := flaggertest.New(t)
|
||||
storage := tracestelemetryschema.NewStorage()
|
||||
storage := tracestelemetryschema.NewStorage(nil)
|
||||
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
mockMetadataStore.KeysMap = tracestelemetryschema.BuildCompleteFieldKeyMap(releaseTime)
|
||||
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, storage, fl, telemetrytypes.SignalTraces)
|
||||
@@ -1026,7 +1026,7 @@ func TestStatementBuilderListQueryWithCorruptData(t *testing.T) {
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
fl := flaggertest.New(t)
|
||||
storage := tracestelemetryschema.NewStorage()
|
||||
storage := tracestelemetryschema.NewStorage(nil)
|
||||
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
mockMetadataStore.KeysMap = c.keysMap
|
||||
if mockMetadataStore.KeysMap == nil {
|
||||
@@ -1099,7 +1099,7 @@ func TestStatementBuilderGroupByResourceEvolution(t *testing.T) {
|
||||
}
|
||||
|
||||
fl := flaggertest.New(t)
|
||||
storage := tracestelemetryschema.NewStorage()
|
||||
storage := tracestelemetryschema.NewStorage(nil)
|
||||
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
mockMetadataStore.KeysMap = tracestelemetryschema.BuildCompleteFieldKeyMap(releaseTime)
|
||||
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, storage, fl, telemetrytypes.SignalTraces)
|
||||
@@ -1264,7 +1264,7 @@ func TestStatementBuilderTraceQuery(t *testing.T) {
|
||||
}
|
||||
|
||||
fl := flaggertest.New(t)
|
||||
storage := tracestelemetryschema.NewStorage()
|
||||
storage := tracestelemetryschema.NewStorage(nil)
|
||||
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
mockMetadataStore.KeysMap = tracestelemetryschema.BuildCompleteFieldKeyMap(releaseTime)
|
||||
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, storage, fl, telemetrytypes.SignalTraces)
|
||||
@@ -1900,7 +1900,7 @@ func newSkipResourceFingerprintBuilder(
|
||||
t.Helper()
|
||||
|
||||
fl := flaggertest.New(t)
|
||||
storage := tracestelemetryschema.NewStorage()
|
||||
storage := tracestelemetryschema.NewStorage(nil)
|
||||
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
||||
mockMetadataStore.KeysMap = tracestelemetryschema.BuildCompleteFieldKeyMap(releaseTime)
|
||||
@@ -1926,7 +1926,7 @@ func TestStatementBuilderGroupByUnseenKey(t *testing.T) {
|
||||
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
||||
|
||||
fl := flaggertest.New(t)
|
||||
storage := tracestelemetryschema.NewStorage()
|
||||
storage := tracestelemetryschema.NewStorage(nil)
|
||||
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
mockMetadataStore.KeysMap = tracestelemetryschema.BuildCompleteFieldKeyMap(releaseTime)
|
||||
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, storage, fl, telemetrytypes.SignalTraces)
|
||||
@@ -1966,7 +1966,7 @@ func TestStatementBuilderAggregationUnseenKey(t *testing.T) {
|
||||
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
||||
|
||||
fl := flaggertest.New(t)
|
||||
storage := tracestelemetryschema.NewStorage()
|
||||
storage := tracestelemetryschema.NewStorage(nil)
|
||||
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
mockMetadataStore.KeysMap = tracestelemetryschema.BuildCompleteFieldKeyMap(releaseTime)
|
||||
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, storage, fl, telemetrytypes.SignalTraces)
|
||||
@@ -2039,7 +2039,7 @@ func TestStatementBuilderSemconvFamilies(t *testing.T) {
|
||||
fl := flaggertest.WithBooleanFlags(t, map[string]bool{
|
||||
flagger.FeatureResolveSemconvFamilies.String(): c.flag,
|
||||
})
|
||||
storage := tracestelemetryschema.NewStorage()
|
||||
storage := tracestelemetryschema.NewStorage(nil)
|
||||
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
mockMetadataStore.KeysMap = tracestelemetryschema.BuildCompleteFieldKeyMap(releaseTime)
|
||||
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, storage, fl, telemetrytypes.SignalTraces)
|
||||
@@ -2071,7 +2071,7 @@ func TestStatementBuilderSemconvSingleSpelling(t *testing.T) {
|
||||
fl := flaggertest.WithBooleanFlags(t, map[string]bool{
|
||||
flagger.FeatureResolveSemconvFamilies.String(): true,
|
||||
})
|
||||
storage := tracestelemetryschema.NewStorage()
|
||||
storage := tracestelemetryschema.NewStorage(nil)
|
||||
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
mockMetadataStore.KeysMap = map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"deployment.environment": {{
|
||||
|
||||
@@ -20,7 +20,7 @@ func newTestTraceOperatorStatementBuilder(t *testing.T) *traceOperatorStatementB
|
||||
t.Helper()
|
||||
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
||||
fl := flaggertest.New(t)
|
||||
storage := tracestelemetryschema.NewStorage()
|
||||
storage := tracestelemetryschema.NewStorage(nil)
|
||||
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
mockMetadataStore.KeysMap = tracestelemetryschema.BuildCompleteFieldKeyMap(releaseTime)
|
||||
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, storage, fl, telemetrytypes.SignalTraces)
|
||||
|
||||
@@ -36,12 +36,13 @@ var _ qbtypes.TraceOperatorStatementBuilder = (*traceOperatorStatementBuilder)(n
|
||||
func NewOperatorFactory(
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
metadataStore telemetrytypes.MetadataStore,
|
||||
schemaMetadataStore telemetrytypes.SchemaMetadataStore,
|
||||
fl flagger.Flagger,
|
||||
) factory.ProviderFactory[qbtypes.TraceOperatorStatementBuilder, statementbuilder.Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("traceoperator"),
|
||||
func(_ context.Context, settings factory.ProviderSettings, cfg statementbuilder.Config) (qbtypes.TraceOperatorStatementBuilder, error) {
|
||||
storage := tracestelemetryschema.NewStorage()
|
||||
storage := tracestelemetryschema.NewStorage(schemaMetadataStore)
|
||||
aggExprRewriter := querybuilder.NewAggExprRewriter(settings, nil, storage, fl, telemetrytypes.SignalTraces)
|
||||
traceStmtBuilder := NewTraceQueryStatementBuilder(
|
||||
settings, metadataStore, storage, aggExprRewriter, telemetryStore, fl,
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestTraceTimeRangeOptimization(t *testing.T) {
|
||||
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
||||
|
||||
fl := flaggertest.New(t)
|
||||
storage := tracestelemetryschema.NewStorage()
|
||||
storage := tracestelemetryschema.NewStorage(nil)
|
||||
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
|
||||
mockMetadataStore.KeysMap = tracestelemetryschema.BuildCompleteFieldKeyMap(releaseTime)
|
||||
|
||||
@@ -2442,10 +2442,10 @@ func (t *telemetryMetaStore) fetchMeterSourceMetricsTemporalityAndType(ctx conte
|
||||
return temporalities, types, nil
|
||||
}
|
||||
|
||||
func (k *telemetryMetaStore) fetchEvolutionEntryFromClickHouse(ctx context.Context, selectors []*telemetrytypes.EvolutionSelector) ([]*telemetrytypes.EvolutionEntry, error) {
|
||||
func fetchEvolutionEntryFromClickHouse(ctx context.Context, telemetryStore telemetrystore.TelemetryStore, table string, selectors []*telemetrytypes.EvolutionSelector) ([]*telemetrytypes.EvolutionEntry, error) {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("signal", "column_name", "column_type", "field_context", "field_name", "version", "release_time")
|
||||
sb.From(fmt.Sprintf("%s.%s", k.relatedMetadataDBName, k.columnEvolutionMetadataTblName))
|
||||
sb.From(table)
|
||||
sb.OrderBy("release_time ASC")
|
||||
|
||||
var clauses []string
|
||||
@@ -2467,7 +2467,7 @@ func (k *telemetryMetaStore) fetchEvolutionEntryFromClickHouse(ctx context.Conte
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
var entries []*telemetrytypes.EvolutionEntry
|
||||
rows, err := k.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
|
||||
rows, err := telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2517,7 +2517,7 @@ func (k *telemetryMetaStore) updateColumnEvolutionMetadataForKeys(ctx context.Co
|
||||
metadataKeySelectors = append(metadataKeySelectors, selector)
|
||||
}
|
||||
|
||||
evolutions, err := k.fetchEvolutionEntryFromClickHouse(ctx, metadataKeySelectors)
|
||||
evolutions, err := fetchEvolutionEntryFromClickHouse(ctx, k.telemetrystore, fmt.Sprintf("%s.%s", k.relatedMetadataDBName, k.columnEvolutionMetadataTblName), metadataKeySelectors)
|
||||
if err != nil {
|
||||
return errors.Newf(errors.TypeInternal, errors.CodeInternal, "failed to fetch evolution from clickhouse %s", err.Error())
|
||||
}
|
||||
|
||||
49
pkg/telemetrymetadata/schema_metadata_store.go
Normal file
49
pkg/telemetrymetadata/schema_metadata_store.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package telemetrymetadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
var CodeFailLoadBaseColumnEvolutions = errors.MustNewCode("fail_load_base_column_evolutions")
|
||||
|
||||
type schemaMetadataStore struct {
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
cacheTTL time.Duration
|
||||
baseColumnEvolutions sync.Map
|
||||
}
|
||||
|
||||
type cachedEvolutions struct {
|
||||
entries []*telemetrytypes.EvolutionEntry
|
||||
fetchedAt time.Time
|
||||
}
|
||||
|
||||
// A zero cacheTTL caches for the life of the process.
|
||||
func NewSchemaMetadataStore(telemetryStore telemetrystore.TelemetryStore, cacheTTL time.Duration) telemetrytypes.SchemaMetadataStore {
|
||||
return &schemaMetadataStore{telemetryStore: telemetryStore, cacheTTL: cacheTTL}
|
||||
}
|
||||
|
||||
// An empty result is not cached: the migrator can add the entries at runtime.
|
||||
func (s *schemaMetadataStore) GetBaseColumnEvolutions(ctx context.Context, signal telemetrytypes.Signal, fieldContext telemetrytypes.FieldContext) ([]*telemetrytypes.EvolutionEntry, error) {
|
||||
selector := &telemetrytypes.EvolutionSelector{Signal: signal, FieldContext: fieldContext, FieldName: telemetrytypes.EvolutionFieldNameAll}
|
||||
if cached, ok := s.baseColumnEvolutions.Load(selector.QualifiedName()); ok {
|
||||
if cached := cached.(cachedEvolutions); s.cacheTTL == 0 || time.Since(cached.fetchedAt) < s.cacheTTL {
|
||||
return cached.entries, nil
|
||||
}
|
||||
}
|
||||
|
||||
entries, err := fetchEvolutionEntryFromClickHouse(ctx, s.telemetryStore, fmt.Sprintf("%s.%s", DBName, ColumnEvolutionMetadataTableName), []*telemetrytypes.EvolutionSelector{selector})
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, CodeFailLoadBaseColumnEvolutions, "failed to fetch the base column evolutions of %s", selector.QualifiedName())
|
||||
}
|
||||
if len(entries) > 0 {
|
||||
s.baseColumnEvolutions.Store(selector.QualifiedName(), cachedEvolutions{entries: entries, fetchedAt: time.Now()})
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
88
pkg/telemetrymetadata/schema_metadata_store_test.go
Normal file
88
pkg/telemetrymetadata/schema_metadata_store_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package telemetrymetadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cmock "github.com/SigNoz/clickhouse-go-mock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
var evolutionColumns = []cmock.ColumnType{
|
||||
{Name: "signal", Type: "String"},
|
||||
{Name: "column_name", Type: "String"},
|
||||
{Name: "column_type", Type: "String"},
|
||||
{Name: "field_context", Type: "String"},
|
||||
{Name: "field_name", Type: "String"},
|
||||
{Name: "version", Type: "UInt32"},
|
||||
{Name: "release_time", Type: "Float64"},
|
||||
}
|
||||
|
||||
func expectBaseColumnEvolutionsQuery(mock cmock.ClickConnMockCommon, rows [][]any) {
|
||||
mock.ExpectQuery(`SELECT signal, column_name, column_type, field_context, field_name, version, release_time FROM signoz_metadata\.distributed_column_evolution_metadata`).
|
||||
WithArgs(telemetrytypes.SignalTraces, telemetrytypes.FieldContextAttribute, "__all__", "__all__").
|
||||
WillReturnRows(cmock.NewRows(evolutionColumns, rows))
|
||||
}
|
||||
|
||||
func TestGetBaseColumnEvolutionsReadsCache(t *testing.T) {
|
||||
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, ®exMatcher{})
|
||||
mock := mockTelemetryStore.Mock()
|
||||
store := NewSchemaMetadataStore(mockTelemetryStore, 0)
|
||||
|
||||
releaseTime := time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)
|
||||
expectBaseColumnEvolutionsQuery(mock, [][]any{
|
||||
{"traces", "attributes", "JSON()", "attribute", "__all__", uint32(0), float64(releaseTime.UnixNano())},
|
||||
})
|
||||
|
||||
for range 2 {
|
||||
entries, err := store.GetBaseColumnEvolutions(context.Background(), telemetrytypes.SignalTraces, telemetrytypes.FieldContextAttribute)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, entries, 1)
|
||||
assert.Equal(t, "attributes", entries[0].ColumnName)
|
||||
assert.Equal(t, releaseTime, entries[0].ReleaseTime.UTC())
|
||||
}
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestGetBaseColumnEvolutionsRefetchesEmptyResult(t *testing.T) {
|
||||
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, ®exMatcher{})
|
||||
mock := mockTelemetryStore.Mock()
|
||||
store := NewSchemaMetadataStore(mockTelemetryStore, 0)
|
||||
get := func() []*telemetrytypes.EvolutionEntry {
|
||||
entries, err := store.GetBaseColumnEvolutions(context.Background(), telemetrytypes.SignalTraces, telemetrytypes.FieldContextAttribute)
|
||||
require.NoError(t, err)
|
||||
return entries
|
||||
}
|
||||
|
||||
expectBaseColumnEvolutionsQuery(mock, nil)
|
||||
assert.Empty(t, get())
|
||||
|
||||
expectBaseColumnEvolutionsQuery(mock, [][]any{
|
||||
{"traces", "attributes", "JSON()", "attribute", "__all__", uint32(0), float64(time.Now().UnixNano())},
|
||||
})
|
||||
assert.Len(t, get(), 1)
|
||||
assert.Len(t, get(), 1, "a found result is cached")
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestGetBaseColumnEvolutionsRefetchesExpiredEntry(t *testing.T) {
|
||||
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, ®exMatcher{})
|
||||
mock := mockTelemetryStore.Mock()
|
||||
store := NewSchemaMetadataStore(mockTelemetryStore, time.Nanosecond)
|
||||
|
||||
rows := [][]any{{"traces", "attributes", "JSON()", "attribute", "__all__", uint32(0), float64(time.Now().UnixNano())}}
|
||||
expectBaseColumnEvolutionsQuery(mock, rows)
|
||||
expectBaseColumnEvolutionsQuery(mock, rows)
|
||||
for range 2 {
|
||||
entries, err := store.GetBaseColumnEvolutions(context.Background(), telemetrytypes.SignalTraces, telemetrytypes.FieldContextAttribute)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, entries, 1)
|
||||
}
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
@@ -288,7 +288,7 @@ func TestConditionFor(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
|
||||
for _, tc := range testCases {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
@@ -374,7 +374,7 @@ func TestConditionForResourceWithEvolution(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
|
||||
for _, tc := range testCases {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
@@ -396,7 +396,7 @@ func TestConditionForResourceWithEvolution(t *testing.T) {
|
||||
// undeclared (Dynamic) paths and genuinely NULL when absent.
|
||||
func TestConditionForScopeIntrinsicFields(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -482,7 +482,7 @@ func TestConditionForScopeIntrinsicFields(t *testing.T) {
|
||||
// user input and queries anyway, emitting a warning instead of failing.
|
||||
func TestConditionForSynthesizedKeys(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
|
||||
// no metadata matches -> the builder must synthesize from user input
|
||||
var noMatches map[string][]*telemetrytypes.TelemetryFieldKey
|
||||
@@ -692,7 +692,7 @@ func TestConditionForSynthesizedKeys(t *testing.T) {
|
||||
// attribute, with or without a metadata entry for the column.
|
||||
func TestIntrinsicColumnFirst(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
q := qbtypes.QueryInfo{StartNs: 1761437108000000000, EndNs: 1761458708000000000}
|
||||
attr := func(name string, dataType telemetrytypes.FieldDataType) *telemetrytypes.TelemetryFieldKey {
|
||||
return &telemetrytypes.TelemetryFieldKey{Name: name, Signal: telemetrytypes.SignalTraces, FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: dataType}
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestConditionForFamilyMergesMembersCurrentFirst(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
fl := familyFlagOn(t)
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
|
||||
// The requested spelling is the old name; precedence must still be
|
||||
// current-first.
|
||||
@@ -87,7 +87,7 @@ func TestConditionForFamilyNegativeKeepsKeylessRows(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
fl := familyFlagOn(t)
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
conds, _, err := querybuilder.Conditions(context.Background(), querybuilder.NewQueryInfo(context.Background(), valuer.UUID{}, fl, telemetrytypes.SignalTraces, nil, startNs, endNs), storage, &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"}, qbtypes.FilterOperatorNotEqual, "production", fieldKeys, false, sb)
|
||||
@@ -122,7 +122,7 @@ func TestConditionForFamilyExists(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
fl := familyFlagOn(t)
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
conds, _, err := querybuilder.Conditions(context.Background(), querybuilder.NewQueryInfo(context.Background(), valuer.UUID{}, fl, telemetrytypes.SignalTraces, nil, startNs, endNs), storage, &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"}, qbtypes.FilterOperatorNotExists, nil, fieldKeys, false, sb)
|
||||
@@ -157,7 +157,7 @@ func TestConditionForFamilyOffByDefault(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
fl := flaggertest.New(t)
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
conds, _, err := querybuilder.Conditions(context.Background(), querybuilder.NewQueryInfo(context.Background(), valuer.UUID{}, fl, telemetrytypes.SignalTraces, nil, startNs, endNs), storage, &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"}, qbtypes.FilterOperatorEqual, "production", fieldKeys, false, sb)
|
||||
@@ -193,7 +193,7 @@ func TestConditionForSingleMemberIsUnchanged(t *testing.T) {
|
||||
}
|
||||
delete(fieldKeys, "deployment.environment")
|
||||
fl := familyFlagOn(t)
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
conds, _, err := querybuilder.Conditions(context.Background(), querybuilder.NewQueryInfo(context.Background(), valuer.UUID{}, fl, telemetrytypes.SignalTraces, nil, startNs, endNs), storage, &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"}, qbtypes.FilterOperatorEqual, "production", fieldKeys, false, sb)
|
||||
@@ -226,7 +226,7 @@ func TestColumnExpressionForFamilyGroupBy(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
fl := familyFlagOn(t)
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
|
||||
expr, err := querybuilder.ResolveColumn(context.Background(), querybuilder.NewQueryInfo(context.Background(), valuer.UUID{}, fl, telemetrytypes.SignalTraces, nil, startNs, endNs), storage, &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"}, telemetrytypes.FieldDataTypeString, fieldKeys)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
func TestSpanScopeFilterExpression(t *testing.T) {
|
||||
// Test that span scope fields work in filter expressions
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -125,7 +125,7 @@ func TestSpanScopeWithResourceFilter(t *testing.T) {
|
||||
// For now, just verify the expression parses correctly
|
||||
// In a real implementation, we'd need to check that the resource filter
|
||||
// is properly skipped when span scope fields are present
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
|
||||
// Prepare field keys for the test
|
||||
fieldKeys := make(map[string][]*telemetrytypes.TelemetryFieldKey)
|
||||
|
||||
@@ -162,12 +162,14 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
type storage struct{}
|
||||
type storage struct {
|
||||
schemaMetadataStore telemetrytypes.SchemaMetadataStore
|
||||
}
|
||||
|
||||
var _ qbtypes.Storage = (*storage)(nil)
|
||||
|
||||
func NewStorage() qbtypes.Storage {
|
||||
return &storage{}
|
||||
func NewStorage(schemaMetadataStore telemetrytypes.SchemaMetadataStore) qbtypes.Storage {
|
||||
return &storage{schemaMetadataStore: schemaMetadataStore}
|
||||
}
|
||||
|
||||
func (m *storage) getColumn(
|
||||
@@ -562,9 +564,35 @@ func (m *storage) Fallback(ctx context.Context, q qbtypes.QueryInfo, key *teleme
|
||||
literal := telemetrytypes.NewTelemetryFieldKey(key.FieldContext.StringValue()+"."+key.Name, key.FieldContext, key.FieldDataType)
|
||||
keys = append(querybuilder.SynthesizeKeys(key, value), querybuilder.SynthesizeKeys(literal, value)...)
|
||||
}
|
||||
if err := m.addBaseColumnEvolutions(ctx, q, keys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return querybuilder.WrapAsLogicalFields(key.Name, keys), nil
|
||||
}
|
||||
|
||||
func (m *storage) addBaseColumnEvolutions(ctx context.Context, q qbtypes.QueryInfo, keys []*telemetrytypes.TelemetryFieldKey) error {
|
||||
if !q.TraceAttrsJSONOn || m.schemaMetadataStore == nil {
|
||||
return nil
|
||||
}
|
||||
var attributeKeys []*telemetrytypes.TelemetryFieldKey
|
||||
for _, key := range keys {
|
||||
if key.FieldContext == telemetrytypes.FieldContextAttribute && len(key.Evolutions) == 0 {
|
||||
attributeKeys = append(attributeKeys, key)
|
||||
}
|
||||
}
|
||||
if len(attributeKeys) == 0 {
|
||||
return nil
|
||||
}
|
||||
evolutions, err := m.schemaMetadataStore.GetBaseColumnEvolutions(ctx, telemetrytypes.SignalTraces, telemetrytypes.FieldContextAttribute)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, key := range attributeKeys {
|
||||
key.Evolutions = evolutions
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// stampColumnType gives an untyped column key the data type its column reads
|
||||
// as, so the intrinsic-column step can compare it with same-named metadata.
|
||||
func stampColumnType(key *telemetrytypes.TelemetryFieldKey, columns []*schema.Column) *telemetrytypes.TelemetryFieldKey {
|
||||
|
||||
@@ -8,8 +8,10 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -44,7 +46,7 @@ func attrKey(name string, dt telemetrytypes.FieldDataType, evo []*telemetrytypes
|
||||
// cast, straddling a dual-read multiIf with the JSON column first.
|
||||
func TestFieldForAttributeJSONEvolution(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
evo := MockAttributeEvolutionData(attrJSONRelease)
|
||||
|
||||
testCases := []struct {
|
||||
@@ -79,7 +81,7 @@ func TestFieldForAttributeJSONEvolution(t *testing.T) {
|
||||
// entry is registered: a key with no evolutions resolves to the Map column for every window.
|
||||
func TestFieldForAttributeNoEvolutionParity(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
|
||||
for _, dt := range []struct {
|
||||
dataType telemetrytypes.FieldDataType
|
||||
@@ -101,7 +103,7 @@ func TestFieldForAttributeNoEvolutionParity(t *testing.T) {
|
||||
// every window.
|
||||
func TestAttributeJSONFlagOffParity(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
evo := MockAttributeEvolutionData(attrJSONRelease)
|
||||
windows := map[string][2]uint64{
|
||||
"BeforeRelease": attrWindowBefore,
|
||||
@@ -154,7 +156,7 @@ func TestAttributeJSONFlagOffParity(t *testing.T) {
|
||||
// numeric comparisons keep numeric semantics; existence never tests the ::String cast.
|
||||
func TestConditionForAttributeJSON(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
evo := MockAttributeEvolutionData(attrJSONRelease)
|
||||
|
||||
testCases := []struct {
|
||||
@@ -261,7 +263,7 @@ func TestConditionForAttributeJSON(t *testing.T) {
|
||||
// home is excluded (De Morgan), including rows that predate the JSON column.
|
||||
func TestConditionForAttributeJSONNotExistsDualRead(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
evo := MockAttributeEvolutionData(attrJSONRelease)
|
||||
|
||||
key := attrKey("user.id", telemetrytypes.FieldDataTypeString, evo)
|
||||
@@ -281,7 +283,7 @@ func TestConditionForAttributeJSONNotExistsDualRead(t *testing.T) {
|
||||
// path is NULL rather than a spurious ”/0, and the numeric branch keeps its toFloat64 coercion.
|
||||
func TestColumnExpressionForAttributeJSON(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
evo := MockAttributeEvolutionData(attrJSONRelease)
|
||||
|
||||
t.Run("group by string", func(t *testing.T) {
|
||||
@@ -304,7 +306,7 @@ func TestColumnExpressionForAttributeJSON(t *testing.T) {
|
||||
// attribute filter must not emit the "ambiguous key" warning.
|
||||
func TestAttributeJSONNoAmbiguityWarning(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
evo := MockAttributeEvolutionData(attrJSONRelease)
|
||||
|
||||
key := attrKey("user.id", telemetrytypes.FieldDataTypeString, evo)
|
||||
@@ -320,7 +322,7 @@ func TestAttributeJSONNoAmbiguityWarning(t *testing.T) {
|
||||
// warning. In the JSON column the two branches share the raw path; only the cast differs.
|
||||
func TestConditionForAttributeJSONTypeCollision(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
evo := MockAttributeEvolutionData(attrJSONRelease)
|
||||
|
||||
strKey := attrKey("http.status_code", telemetrytypes.FieldDataTypeString, evo)
|
||||
@@ -350,7 +352,7 @@ func TestConditionForAttributeJSONTypeCollision(t *testing.T) {
|
||||
// as its actual stored type.
|
||||
func TestColumnExpressionForAttributeJSONTypeCollision(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
evo := MockAttributeEvolutionData(attrJSONRelease)
|
||||
|
||||
strKey := attrKey("http.status_code", telemetrytypes.FieldDataTypeString, evo)
|
||||
@@ -374,7 +376,7 @@ func TestColumnExpressionForAttributeJSONTypeCollision(t *testing.T) {
|
||||
// coverage.
|
||||
func TestColumnExpressionForAttributeJSONTypeCollisionNumericAgg(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
evo := MockAttributeEvolutionData(attrJSONRelease)
|
||||
|
||||
numKey := attrKey("http.status_code", telemetrytypes.FieldDataTypeNumber, evo)
|
||||
@@ -396,7 +398,7 @@ func TestColumnExpressionForAttributeJSONTypeCollisionNumericAgg(t *testing.T) {
|
||||
// columns (attributes_string / attributes_number), each with its own mapContains guard.
|
||||
func TestConditionForAttributeMapTypeCollisionParity(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
evo := MockAttributeEvolutionData(attrJSONRelease)
|
||||
|
||||
strKey := attrKey("http.status_code", telemetrytypes.FieldDataTypeString, evo)
|
||||
@@ -440,7 +442,7 @@ func TestColumnForUnspecifiedAttributeNoBranchFlip(t *testing.T) {
|
||||
// evolution: a key without it (the pre-rollout system) is byte-identical to today.
|
||||
func TestConditionForAttributeJSONNegativeOperatorParity(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
evo := MockAttributeEvolutionData(attrJSONRelease)
|
||||
|
||||
build := func(t *testing.T, key telemetrytypes.TelemetryFieldKey, window [2]uint64, op qbtypes.FilterOperator, value any) string {
|
||||
@@ -520,7 +522,7 @@ func TestConditionForAttributeJSONNegativeOperatorParity(t *testing.T) {
|
||||
// EXISTS/NOT EXISTS must still exclude a key absent from every home, rather than matching it.
|
||||
func TestConditionForAttributeJSONStraddleAbsentKeyExclusion(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
evo := MockAttributeEvolutionData(attrJSONRelease)
|
||||
|
||||
build := func(t *testing.T, key telemetrytypes.TelemetryFieldKey, op qbtypes.FilterOperator, value any) string {
|
||||
@@ -551,3 +553,139 @@ func TestConditionForAttributeJSONStraddleAbsentKeyExclusion(t *testing.T) {
|
||||
assert.Contains(t, sql, ", NULL) IS NULL")
|
||||
})
|
||||
}
|
||||
|
||||
func TestConditionForAttributeJSONUnknownKey(t *testing.T) {
|
||||
evolutions := MockAttributeEvolutionData(attrJSONRelease)
|
||||
bare := telemetrytypes.TelemetryFieldKey{Name: "unknown.attr"}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
window [2]uint64
|
||||
flagOff bool
|
||||
evolutions []*telemetrytypes.EvolutionEntry
|
||||
key telemetrytypes.TelemetryFieldKey
|
||||
operator qbtypes.FilterOperator
|
||||
value any
|
||||
wantContains []string
|
||||
wantNotContains []string
|
||||
}{
|
||||
{
|
||||
name: "BeforeRollout_ReadsMap",
|
||||
window: attrWindowBefore,
|
||||
evolutions: evolutions,
|
||||
key: bare,
|
||||
operator: qbtypes.FilterOperatorEqual,
|
||||
value: "x",
|
||||
wantContains: []string{"attributes_string['unknown.attr'] = ?"},
|
||||
wantNotContains: []string{"attributes.`unknown.attr`"},
|
||||
},
|
||||
{
|
||||
name: "AfterRollout_ReadsJSON",
|
||||
window: attrWindowAfter,
|
||||
evolutions: evolutions,
|
||||
key: bare,
|
||||
operator: qbtypes.FilterOperatorEqual,
|
||||
value: "x",
|
||||
wantContains: []string{"attributes.`unknown.attr`::String = ?"},
|
||||
wantNotContains: []string{"attributes_string"},
|
||||
},
|
||||
{
|
||||
name: "StraddlingRollout_ReadsJSONThenMap",
|
||||
window: attrWindowStraddle,
|
||||
evolutions: evolutions,
|
||||
key: bare,
|
||||
operator: qbtypes.FilterOperatorEqual,
|
||||
value: "x",
|
||||
wantContains: []string{"multiIf(attributes.`unknown.attr` IS NOT NULL, attributes.`unknown.attr`::String, mapContains(attributes_string, 'unknown.attr'), attributes_string['unknown.attr'], NULL) = ?"},
|
||||
},
|
||||
{
|
||||
name: "AfterRollout_NumberOperand_ReadsJSON",
|
||||
window: attrWindowAfter,
|
||||
evolutions: evolutions,
|
||||
key: bare,
|
||||
operator: qbtypes.FilterOperatorEqual,
|
||||
value: float64(1),
|
||||
wantContains: []string{"toFloat64OrNull(attributes.`unknown.attr`::String) = ?"},
|
||||
wantNotContains: []string{"attributes_number"},
|
||||
},
|
||||
{
|
||||
name: "AfterRollout_Exists_ReadsJSONOnly",
|
||||
window: attrWindowAfter,
|
||||
evolutions: evolutions,
|
||||
key: bare,
|
||||
operator: qbtypes.FilterOperatorExists,
|
||||
wantContains: []string{"attributes.`unknown.attr` IS NOT NULL"},
|
||||
wantNotContains: []string{"mapContains"},
|
||||
},
|
||||
{
|
||||
name: "AfterRollout_AttributeContext_ReadsJSONForBothSpellings",
|
||||
window: attrWindowAfter,
|
||||
evolutions: evolutions,
|
||||
key: telemetrytypes.TelemetryFieldKey{Name: "unknown.attr", FieldContext: telemetrytypes.FieldContextAttribute},
|
||||
operator: qbtypes.FilterOperatorEqual,
|
||||
value: "x",
|
||||
wantContains: []string{"attributes.`unknown.attr`::String = ?", "attributes.`attribute.unknown.attr`::String = ?"},
|
||||
wantNotContains: []string{"attributes_string"},
|
||||
},
|
||||
{
|
||||
name: "AfterRollout_SpanContext_ReadsJSONAttribute",
|
||||
window: attrWindowAfter,
|
||||
evolutions: evolutions,
|
||||
key: telemetrytypes.TelemetryFieldKey{Name: "unknown.attr", FieldContext: telemetrytypes.FieldContextSpan},
|
||||
operator: qbtypes.FilterOperatorEqual,
|
||||
value: "x",
|
||||
wantContains: []string{"attributes.`unknown.attr`::String = ?"},
|
||||
wantNotContains: []string{"attributes_string"},
|
||||
},
|
||||
{
|
||||
name: "NoEvolutions_ReadsMap",
|
||||
window: attrWindowAfter,
|
||||
key: bare,
|
||||
operator: qbtypes.FilterOperatorEqual,
|
||||
value: "x",
|
||||
wantContains: []string{"attributes_string['unknown.attr'] = ?"},
|
||||
wantNotContains: []string{"attributes.`unknown.attr`"},
|
||||
},
|
||||
{
|
||||
name: "FlagOff_ReadsMap",
|
||||
window: attrWindowAfter,
|
||||
flagOff: true,
|
||||
evolutions: evolutions,
|
||||
key: bare,
|
||||
operator: qbtypes.FilterOperatorEqual,
|
||||
value: "x",
|
||||
wantContains: []string{"attributes_string['unknown.attr'] = ?"},
|
||||
wantNotContains: []string{"attributes.`unknown.attr`"},
|
||||
},
|
||||
{
|
||||
name: "ResourceContext_IgnoresAttributeEvolutions",
|
||||
window: attrWindowAfter,
|
||||
evolutions: evolutions,
|
||||
key: telemetrytypes.TelemetryFieldKey{Name: "unknown.attr", FieldContext: telemetrytypes.FieldContextResource},
|
||||
operator: qbtypes.FilterOperatorEqual,
|
||||
value: "x",
|
||||
wantContains: []string{"resource.`unknown.attr`::String", "resources_string['unknown.attr']"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
schemaMetadataStore := telemetrytypestest.NewMockSchemaMetadataStore(t)
|
||||
schemaMetadataStore.EXPECT().GetBaseColumnEvolutions(mock.Anything, telemetrytypes.SignalTraces, telemetrytypes.FieldContextAttribute).Return(testCase.evolutions, nil).Maybe()
|
||||
|
||||
q := qbtypes.QueryInfo{StartNs: testCase.window[0], EndNs: testCase.window[1], TraceAttrsJSONOn: !testCase.flagOff}
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
conds, _, err := querybuilder.Conditions(context.Background(), q, NewStorage(schemaMetadataStore), &testCase.key, testCase.operator, testCase.value, map[string][]*telemetrytypes.TelemetryFieldKey{}, false, sb)
|
||||
require.NoError(t, err)
|
||||
sb.Where(conds...)
|
||||
sql, _ := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
for _, want := range testCase.wantContains {
|
||||
assert.Contains(t, sql, want)
|
||||
}
|
||||
for _, unwanted := range testCase.wantNotContains {
|
||||
assert.NotContains(t, sql, unwanted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ var (
|
||||
// `attributes_promoted` alone after promotion — fanning out only across an evolution boundary.
|
||||
func TestFieldForAttributePromotedEvolution(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
evo := MockPromotedAttributeEvolutionData("span.operation", promoJSONRelease, promoPromoRelease)
|
||||
|
||||
win := func(from, to string) [2]uint64 {
|
||||
@@ -65,7 +65,7 @@ func TestFieldForAttributePromotedEvolution(t *testing.T) {
|
||||
// attributes_promoted_paths_tokenbf) — not the attributes column.
|
||||
func TestConditionForAttributePromoted(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
evo := MockPromotedAttributeEvolutionData("span.operation", promoJSONRelease, promoPromoRelease)
|
||||
afterPromo := [2]uint64{
|
||||
uint64(time.Date(2025, 7, 1, 0, 0, 0, 0, time.UTC).UnixNano()),
|
||||
|
||||
@@ -146,7 +146,7 @@ func TestGetFieldKeyName(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
read, err := storage.Read(ctx, qbtypes.QueryInfo{StartNs: uint64(time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC).UnixNano()), EndNs: uint64(time.Date(2024, 6, 5, 0, 0, 0, 0, time.UTC).UnixNano())}, &tc.key)
|
||||
|
||||
if tc.expectedError != nil {
|
||||
@@ -234,7 +234,7 @@ func TestFieldForResourceWithEvolution(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
read, err := storage.Read(ctx, qbtypes.QueryInfo{StartNs: tc.tsStart, EndNs: tc.tsEnd}, &tc.key)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedResult, read.SQL)
|
||||
@@ -294,7 +294,7 @@ func TestColumnExpressionForTemporalColumn(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
result, err := querybuilder.ResolveColumn(ctx, qbtypes.QueryInfo{StartNs: tsStart, EndNs: tsEnd}, storage, &tc.key, tc.requiredDataType, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedResult, result)
|
||||
@@ -322,7 +322,7 @@ func TestColumnExpressionForTimestampAttributeCollision(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
|
||||
t.Run("bare timestamp resolves to the intrinsic column alone", func(t *testing.T) {
|
||||
bare := telemetrytypes.TelemetryFieldKey{Name: "timestamp"}
|
||||
@@ -431,7 +431,7 @@ func TestColumnExpressionForScopeDeclaredPath(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
storage := NewStorage()
|
||||
storage := NewStorage(nil)
|
||||
result, err := querybuilder.ResolveColumn(ctx, qbtypes.QueryInfo{}, storage, &tc.key, telemetrytypes.FieldDataTypeUnspecified, tc.keys)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedResult, result)
|
||||
|
||||
@@ -706,6 +706,12 @@ func (r *QueryRangeRequest) UnmarshalJSON(data []byte) error {
|
||||
// Copy the decoded values back to the original struct
|
||||
*r = QueryRangeRequest(temp)
|
||||
|
||||
// Coerce the window to epoch milliseconds up front so every downstream
|
||||
// consumer (TimeRange, narrowWindowByTraceID, step interval, etc.) can
|
||||
// safely assume ms regardless of the resolution the caller sent.
|
||||
r.Start = ToMilliSecs(r.Start)
|
||||
r.End = ToMilliSecs(r.End)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
25
pkg/types/querybuildertypes/querybuildertypesv5/time.go
Normal file
25
pkg/types/querybuildertypes/querybuildertypesv5/time.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package querybuildertypesv5
|
||||
|
||||
import "math"
|
||||
|
||||
// ToMilliSecs takes an epoch whose resolution is inferred from its magnitude
|
||||
// (s/ms/µs/ns) and returns it in milliseconds. A millisecond epoch for the
|
||||
// current era has 13 digits (e.g. ~1.7e12 in 2026), so the value is scaled so
|
||||
// its digit-width matches: smaller values (seconds) are scaled up, larger ones
|
||||
// (micro/nanoseconds) are scaled down. Zero is returned unchanged.
|
||||
func ToMilliSecs(epoch uint64) uint64 {
|
||||
if epoch == 0 {
|
||||
return 0
|
||||
}
|
||||
temp := epoch
|
||||
count := 0
|
||||
for epoch != 0 {
|
||||
epoch /= 10
|
||||
count++
|
||||
}
|
||||
const msDigits = 13
|
||||
if count < msDigits {
|
||||
return temp * uint64(math.Pow(10, float64(msDigits-count)))
|
||||
}
|
||||
return temp / uint64(math.Pow(10, float64(count-msDigits)))
|
||||
}
|
||||
52
pkg/types/querybuildertypes/querybuildertypesv5/time_test.go
Normal file
52
pkg/types/querybuildertypes/querybuildertypesv5/time_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package querybuildertypesv5
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestToMilliSecs(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
epoch uint64
|
||||
expected uint64
|
||||
}{
|
||||
{
|
||||
name: "SecondsEpoch_TenDigits_ScaledUp",
|
||||
epoch: 1672531200, // seconds, 2023-01-01 00:00:00 UTC
|
||||
expected: 1672531200000, // * 10^3
|
||||
},
|
||||
{
|
||||
name: "MilliSecondsEpoch_ThirteenDigits_Unchanged",
|
||||
epoch: 1672531200000,
|
||||
expected: 1672531200000, // unchanged
|
||||
},
|
||||
{
|
||||
name: "MicroSecondsEpoch_SixteenDigits_ScaledDown",
|
||||
epoch: 1672531200000000, // microseconds
|
||||
expected: 1672531200000, // / 10^3
|
||||
},
|
||||
{
|
||||
name: "NanoSecondsEpoch_NineteenDigits_ScaledDown",
|
||||
epoch: 1672531200000000000, // nanoseconds
|
||||
expected: 1672531200000, // / 10^6
|
||||
},
|
||||
{
|
||||
name: "Zero_Unchanged",
|
||||
epoch: 0,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "SecondsEpoch_RecentTimestamp_ScaledUp",
|
||||
epoch: 1716638400, // 2024-05-25 12:00:00 UTC
|
||||
expected: 1716638400000,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.expected, ToMilliSecs(testCase.epoch))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"math"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
@@ -70,6 +71,9 @@ const (
|
||||
MaxNumBuckets = 512
|
||||
)
|
||||
|
||||
// DefaultMaxEndTimeAhead tolerates client clock skew; an end time further ahead is a malformed timestamp.
|
||||
const DefaultMaxEndTimeAhead = 24 * time.Hour
|
||||
|
||||
// ValidationOption is a functional option for configuring validation behaviour.
|
||||
type ValidationOption func(*validationConfig)
|
||||
|
||||
@@ -82,6 +86,7 @@ type validationConfig struct {
|
||||
skipGroupByValidation bool
|
||||
withTimestampGroupByValidation bool
|
||||
withReduceToValidation bool
|
||||
maxEndTimeAhead time.Duration
|
||||
}
|
||||
|
||||
func applyValidationOptions(opts []ValidationOption) validationConfig {
|
||||
@@ -156,6 +161,13 @@ func WithReduceToValidation() ValidationOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxEndTimeAhead sets how far past now a request's end time may be.
|
||||
func WithMaxEndTimeAhead(d time.Duration) ValidationOption {
|
||||
return func(cfg *validationConfig) {
|
||||
cfg.maxEndTimeAhead = d
|
||||
}
|
||||
}
|
||||
|
||||
// Validate performs preliminary validation on QueryBuilderQuery.
|
||||
func (q *QueryBuilderQuery[T]) Validate(opts ...ValidationOption) error {
|
||||
cfg := applyValidationOptions(opts)
|
||||
@@ -574,12 +586,8 @@ func (q *QueryBuilderQuery[T]) validateOrderByForAggregation() error {
|
||||
|
||||
// Validate validates the entire query range request.
|
||||
func (r *QueryRangeRequest) Validate(opts ...ValidationOption) error {
|
||||
// Validate time range
|
||||
if r.RequestType != RequestTypeRawStream && r.Start >= r.End {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"start time must be before end time",
|
||||
)
|
||||
if err := r.ValidateTimeRange(opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate request type
|
||||
@@ -630,15 +638,14 @@ func (r *QueryRangeRequest) Validate(opts ...ValidationOption) error {
|
||||
// specs) and returns the request type's ValidationOptions. The dry-run path uses
|
||||
// this so per-query errors can be attributed individually via QueryEnvelope.Validate
|
||||
// instead of failing fast like Validate does.
|
||||
func (r *QueryRangeRequest) ValidateRequestScope() ([]ValidationOption, error) {
|
||||
if r.RequestType != RequestTypeRawStream && r.Start >= r.End {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "start time must be before end time")
|
||||
func (r *QueryRangeRequest) ValidateRequestScope(opts ...ValidationOption) ([]ValidationOption, error) {
|
||||
if err := r.ValidateTimeRange(opts...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var opts []ValidationOption
|
||||
switch r.RequestType {
|
||||
case RequestTypeRaw, RequestTypeRawStream, RequestTypeTrace, RequestTypeTimeSeries, RequestTypeScalar, RequestTypeHeatmap:
|
||||
opts = GetValidationOptions(r.RequestType)
|
||||
opts = append(opts, GetValidationOptions(r.RequestType)...)
|
||||
default:
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid request type: %s", r.RequestType).
|
||||
WithAdditional("Valid request types are: raw, timeseries, scalar, heatmap")
|
||||
@@ -699,6 +706,28 @@ func (e QueryEnvelope) Validate(opts ...ValidationOption) error {
|
||||
return validateQueryEnvelope(e, opts...)
|
||||
}
|
||||
|
||||
// ValidateTimeRange checks only the request's time window.
|
||||
func (r *QueryRangeRequest) ValidateTimeRange(opts ...ValidationOption) error {
|
||||
cfg := applyValidationOptions(opts)
|
||||
if r.RequestType == RequestTypeRawStream {
|
||||
return nil
|
||||
}
|
||||
if r.Start == 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "start time is required")
|
||||
}
|
||||
if r.Start >= r.End {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "start time must be before end time")
|
||||
}
|
||||
maxAhead := cfg.maxEndTimeAhead
|
||||
if maxAhead <= 0 {
|
||||
maxAhead = DefaultMaxEndTimeAhead
|
||||
}
|
||||
if r.End > uint64(time.Now().Add(maxAhead).UnixMilli()) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "end time %s is too far in the future", time.UnixMilli(int64(r.End)).UTC().Format(time.RFC3339))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAllQueriesNotDisabled validates that at least one query in the composite query is enabled.
|
||||
func (r *QueryRangeRequest) validateAllQueriesNotDisabled() error {
|
||||
for _, envelope := range r.CompositeQuery.Queries {
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const EvolutionFieldNameAll = "__all__"
|
||||
|
||||
type EvolutionEntry struct {
|
||||
Signal Signal `json:"signal"`
|
||||
ColumnName string `json:"column_name"`
|
||||
|
||||
@@ -7,6 +7,10 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type SchemaMetadataStore interface {
|
||||
GetBaseColumnEvolutions(ctx context.Context, signal Signal, fieldContext FieldContext) ([]*EvolutionEntry, error)
|
||||
}
|
||||
|
||||
// MetadataStore is the interface for the telemetry metadata store.
|
||||
type MetadataStore interface {
|
||||
// GetKeys returns a map of field keys types.TelemetryFieldKey by name, there can be multiple keys with the same name
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package telemetrytypestest
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewMockSchemaMetadataStore creates a new instance of MockSchemaMetadataStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockSchemaMetadataStore(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockSchemaMetadataStore {
|
||||
if helper, ok := t.(interface{ Helper() }); ok {
|
||||
helper.Helper()
|
||||
}
|
||||
|
||||
mock := &MockSchemaMetadataStore{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() {
|
||||
if helper, ok := t.(interface{ Helper() }); ok {
|
||||
helper.Helper()
|
||||
}
|
||||
mock.AssertExpectations(t)
|
||||
})
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// MockSchemaMetadataStore is an autogenerated mock type for the SchemaMetadataStore type
|
||||
type MockSchemaMetadataStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockSchemaMetadataStore_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockSchemaMetadataStore) EXPECT() *MockSchemaMetadataStore_Expecter {
|
||||
return &MockSchemaMetadataStore_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// GetBaseColumnEvolutions provides a mock function for the type MockSchemaMetadataStore
|
||||
func (_mock *MockSchemaMetadataStore) GetBaseColumnEvolutions(ctx context.Context, signal telemetrytypes.Signal, fieldContext telemetrytypes.FieldContext) ([]*telemetrytypes.EvolutionEntry, error) {
|
||||
ret := _mock.Called(ctx, signal, fieldContext)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetBaseColumnEvolutions")
|
||||
}
|
||||
|
||||
var r0 []*telemetrytypes.EvolutionEntry
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, telemetrytypes.Signal, telemetrytypes.FieldContext) ([]*telemetrytypes.EvolutionEntry, error)); ok {
|
||||
return returnFunc(ctx, signal, fieldContext)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, telemetrytypes.Signal, telemetrytypes.FieldContext) []*telemetrytypes.EvolutionEntry); ok {
|
||||
r0 = returnFunc(ctx, signal, fieldContext)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*telemetrytypes.EvolutionEntry)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, telemetrytypes.Signal, telemetrytypes.FieldContext) error); ok {
|
||||
r1 = returnFunc(ctx, signal, fieldContext)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockSchemaMetadataStore_GetBaseColumnEvolutions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBaseColumnEvolutions'
|
||||
type MockSchemaMetadataStore_GetBaseColumnEvolutions_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetBaseColumnEvolutions is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - signal telemetrytypes.Signal
|
||||
// - fieldContext telemetrytypes.FieldContext
|
||||
func (_e *MockSchemaMetadataStore_Expecter) GetBaseColumnEvolutions(ctx any, signal any, fieldContext any) *MockSchemaMetadataStore_GetBaseColumnEvolutions_Call {
|
||||
return &MockSchemaMetadataStore_GetBaseColumnEvolutions_Call{Call: _e.mock.On("GetBaseColumnEvolutions", ctx, signal, fieldContext)}
|
||||
}
|
||||
|
||||
func (_c *MockSchemaMetadataStore_GetBaseColumnEvolutions_Call) Run(run func(ctx context.Context, signal telemetrytypes.Signal, fieldContext telemetrytypes.FieldContext)) *MockSchemaMetadataStore_GetBaseColumnEvolutions_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 telemetrytypes.Signal
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(telemetrytypes.Signal)
|
||||
}
|
||||
var arg2 telemetrytypes.FieldContext
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(telemetrytypes.FieldContext)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSchemaMetadataStore_GetBaseColumnEvolutions_Call) Return(evolutionEntrys []*telemetrytypes.EvolutionEntry, err error) *MockSchemaMetadataStore_GetBaseColumnEvolutions_Call {
|
||||
_c.Call.Return(evolutionEntrys, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSchemaMetadataStore_GetBaseColumnEvolutions_Call) RunAndReturn(run func(ctx context.Context, signal telemetrytypes.Signal, fieldContext telemetrytypes.FieldContext) ([]*telemetrytypes.EvolutionEntry, error)) *MockSchemaMetadataStore_GetBaseColumnEvolutions_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
2
tests/fixtures/signoz.py
vendored
2
tests/fixtures/signoz.py
vendored
@@ -92,6 +92,8 @@ def create_signoz(
|
||||
"SIGNOZ_ALERTMANAGER_SIGNOZ_ROUTE_GROUP__WAIT": "1s",
|
||||
"SIGNOZ_ALERTMANAGER_SIGNOZ_ROUTE_GROUP__INTERVAL": "5s",
|
||||
"SIGNOZ_CLOUDINTEGRATION_AGENT_VERSION": "v0.0.8",
|
||||
# tests reseed the attributes rollout row between cases
|
||||
"SIGNOZ_QUERIER_SCHEMAMETADATASTORE_CACHE__TTL": "1ns",
|
||||
}
|
||||
| sqlstore.env
|
||||
| clickhouse.env
|
||||
|
||||
165
tests/integration/tests/queriercommon/09_time_range.py
Normal file
165
tests/integration/tests/queriercommon/09_time_range.py
Normal file
@@ -0,0 +1,165 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logs import Logs
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import (
|
||||
Aggregation,
|
||||
BuilderQuery,
|
||||
MetricAggregation,
|
||||
get_all_warnings,
|
||||
get_error_message,
|
||||
get_series_values,
|
||||
make_query_request,
|
||||
)
|
||||
from fixtures.traces import TraceIdGenerator, Traces
|
||||
|
||||
TWO_YEARS = timedelta(days=730)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"start_ms,end_ms,message",
|
||||
[
|
||||
pytest.param(0, 1_700_000_000_000, "start time is required", id="zero_start"),
|
||||
pytest.param(1_700_000_000_000, 1_700_000_000_000, "start time must be before end time", id="start_equals_end"),
|
||||
# 2592000000 is 30 days in ms; read as an epoch in seconds it is year 2052.
|
||||
pytest.param(1_700_000_000_000, 2_592_000_000, "end time 2052-02-20T00:00:00Z is too far in the future", id="end_in_2052"),
|
||||
],
|
||||
)
|
||||
def test_time_range_rejected(
|
||||
start_ms: int,
|
||||
end_ms: int,
|
||||
message: str,
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
queries=[BuilderQuery(signal="logs", aggregations=[Aggregation(expression="count()")], step_interval=60).to_dict()],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
assert get_error_message(response.json()) == message
|
||||
|
||||
|
||||
def test_two_year_logs_timeseries_step_clamped(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
) -> None:
|
||||
# A 1s step over 2 years is clamped to 42045s (at most ~1500 points); end is aligned to it so the bucket count is stable.
|
||||
step_seconds = 42045
|
||||
end = datetime.fromtimestamp(int(datetime.now(tz=UTC).timestamp()) // step_seconds * step_seconds, tz=UTC)
|
||||
insert_logs([Logs(timestamp=end - timedelta(minutes=1), resources={"service.name": "time-range-logs"}, body="two year span")])
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((end - TWO_YEARS).timestamp() * 1000),
|
||||
end_ms=int(end.timestamp() * 1000),
|
||||
queries=[
|
||||
BuilderQuery(
|
||||
signal="logs",
|
||||
filter_expression="service.name = 'time-range-logs'",
|
||||
aggregations=[Aggregation(expression="count()")],
|
||||
step_interval=1,
|
||||
).to_dict()
|
||||
],
|
||||
format_options={"formatTableResultForUI": False, "fillGaps": True},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["data"]["meta"]["stepIntervals"] == {"A": step_seconds}
|
||||
assert get_all_warnings(response.json()) == [
|
||||
{"message": f"Query A is requesting aggregation interval 1 seconds, which is smaller than the minimum allowed interval of {step_seconds} seconds for selected time range. Using the minimum instead"},
|
||||
]
|
||||
values = get_series_values(response.json(), "A")
|
||||
assert len(values) == 1502
|
||||
assert sum(v["value"] for v in values) == 1
|
||||
|
||||
|
||||
def test_two_year_traces_timeseries_step_clamped(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
# Same clamp as logs: 42045s; end is aligned to it so the bucket count is stable.
|
||||
step_seconds = 42045
|
||||
end = datetime.fromtimestamp(int(datetime.now(tz=UTC).timestamp()) // step_seconds * step_seconds, tz=UTC)
|
||||
insert_traces([Traces(timestamp=end - timedelta(minutes=1), trace_id=TraceIdGenerator.trace_id(), span_id=TraceIdGenerator.span_id(), resources={"service.name": "time-range-traces"})])
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((end - TWO_YEARS).timestamp() * 1000),
|
||||
end_ms=int(end.timestamp() * 1000),
|
||||
queries=[
|
||||
BuilderQuery(
|
||||
signal="traces",
|
||||
filter_expression="service.name = 'time-range-traces'",
|
||||
aggregations=[Aggregation(expression="count()")],
|
||||
step_interval=1,
|
||||
).to_dict()
|
||||
],
|
||||
format_options={"formatTableResultForUI": False, "fillGaps": True},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["data"]["meta"]["stepIntervals"] == {"A": step_seconds}
|
||||
assert get_all_warnings(response.json()) == [
|
||||
{"message": f"Query A is requesting aggregation interval 1 seconds, which is smaller than the minimum allowed interval of {step_seconds} seconds for selected time range. Using the minimum instead"},
|
||||
]
|
||||
values = get_series_values(response.json(), "A")
|
||||
assert len(values) == 1502
|
||||
assert sum(v["value"] for v in values) == 1
|
||||
|
||||
|
||||
def test_two_year_metrics_timeseries_step_clamped(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
) -> None:
|
||||
# A 1s step over 2 years is clamped to 210600s (~300 points); end is aligned to it so the bucket count is stable.
|
||||
step_seconds = 210600
|
||||
metric_name = "time_range_two_year_gauge"
|
||||
end = datetime.fromtimestamp(int(datetime.now(tz=UTC).timestamp()) // step_seconds * step_seconds, tz=UTC)
|
||||
insert_metrics([Metrics(metric_name=metric_name, labels={"service": "time-range-metrics"}, timestamp=end - timedelta(minutes=1), value=1.0, temporality="Unspecified", type_="Gauge", is_monotonic=False)])
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((end - TWO_YEARS).timestamp() * 1000),
|
||||
end_ms=int(end.timestamp() * 1000),
|
||||
queries=[
|
||||
BuilderQuery(
|
||||
signal="metrics",
|
||||
aggregations=[MetricAggregation(metric_name=metric_name, time_aggregation="latest", space_aggregation="sum", temporality="unspecified")],
|
||||
step_interval=1,
|
||||
).to_dict()
|
||||
],
|
||||
format_options={"formatTableResultForUI": False, "fillGaps": True},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["data"]["meta"]["stepIntervals"] == {"A": step_seconds}
|
||||
assert get_all_warnings(response.json()) == [
|
||||
{"message": f"Query A is requesting aggregation interval 1 seconds, which is smaller than the minimum allowed interval of {step_seconds} seconds for selected time range. Using the minimum instead"},
|
||||
]
|
||||
assert len(get_series_values(response.json(), "A")) == 301
|
||||
@@ -13,8 +13,11 @@ from fixtures.querier import (
|
||||
build_aggregation,
|
||||
build_group_by_field,
|
||||
build_traces_scalar_query,
|
||||
get_all_warnings,
|
||||
get_preview_sql,
|
||||
get_rows,
|
||||
index_series_by_label,
|
||||
make_preview_query_request,
|
||||
make_query_request,
|
||||
)
|
||||
from fixtures.traces import TraceIdGenerator, Traces
|
||||
@@ -467,3 +470,79 @@ def test_traces_attributes_json_list_view(
|
||||
assert row["data"]["name"] == name
|
||||
# int/float compare equal in Python, so map-sourced float64 and JSON-sourced numbers both match.
|
||||
assert row["data"]["attributes"] == attributes, name
|
||||
|
||||
|
||||
def test_traces_attributes_json_unreported_key(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
seed_attribute_evolution: Callable[[str, datetime], None],
|
||||
) -> None:
|
||||
"""An attribute metadata does not report follows the rollout like a reported one: the legacy
|
||||
map before it, the attributes JSON column after it."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
evolution_time = datetime.now(tz=UTC).replace(second=0, microsecond=0) - timedelta(minutes=30)
|
||||
seed_attribute_evolution("traces", evolution_time)
|
||||
|
||||
before = evolution_time - timedelta(minutes=5)
|
||||
after = evolution_time + timedelta(minutes=5)
|
||||
spans = [
|
||||
Traces(
|
||||
timestamp=before,
|
||||
trace_id=TraceIdGenerator.trace_id(),
|
||||
span_id=TraceIdGenerator.span_id(),
|
||||
name="before",
|
||||
attributes={"unreported.key": "value"},
|
||||
attribute_write_mode="legacy_only",
|
||||
),
|
||||
Traces(
|
||||
timestamp=after,
|
||||
trace_id=TraceIdGenerator.trace_id(),
|
||||
span_id=TraceIdGenerator.span_id(),
|
||||
name="after",
|
||||
attributes={"unreported.key": "value"},
|
||||
attribute_write_mode="json_only",
|
||||
),
|
||||
]
|
||||
# no metadata rows, so the querier does not know the key
|
||||
for span in spans:
|
||||
span.attribute_keys = []
|
||||
span.tag_attributes = []
|
||||
insert_traces(spans)
|
||||
|
||||
queries = [
|
||||
build_traces_scalar_query(
|
||||
aggregations=[build_aggregation("count()")],
|
||||
filter_expression="unreported.key = 'value'",
|
||||
)
|
||||
]
|
||||
|
||||
# before the rollout -> legacy map
|
||||
start_ms = int((before - timedelta(minutes=1)).timestamp() * 1000)
|
||||
end_ms = int((before + timedelta(minutes=1)).timestamp() * 1000)
|
||||
|
||||
sql = get_preview_sql(make_preview_query_request(signoz, token, start_ms=start_ms, end_ms=end_ms, queries=queries), "A")
|
||||
assert "attributes_string['unreported.key']" in sql
|
||||
assert "attributes.`unreported.key`" not in sql
|
||||
|
||||
response = make_query_request(signoz, token, start_ms=start_ms, end_ms=end_ms, queries=queries)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
series = response.json()["data"]["data"]["results"][0]["aggregations"][0]["series"]
|
||||
assert [value["value"] for value in series[0]["values"]] == [1]
|
||||
assert any("not found in metadata" in warning.get("message", "") for warning in get_all_warnings(response.json()))
|
||||
|
||||
# after the rollout -> attributes JSON column
|
||||
start_ms = int((after - timedelta(minutes=1)).timestamp() * 1000)
|
||||
end_ms = int((after + timedelta(minutes=1)).timestamp() * 1000)
|
||||
|
||||
sql = get_preview_sql(make_preview_query_request(signoz, token, start_ms=start_ms, end_ms=end_ms, queries=queries), "A")
|
||||
assert "attributes.`unreported.key`" in sql
|
||||
assert "attributes_string" not in sql
|
||||
|
||||
response = make_query_request(signoz, token, start_ms=start_ms, end_ms=end_ms, queries=queries)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
series = response.json()["data"]["data"]["results"][0]["aggregations"][0]["series"]
|
||||
assert [value["value"] for value in series[0]["values"]] == [1]
|
||||
assert any("not found in metadata" in warning.get("message", "") for warning in get_all_warnings(response.json()))
|
||||
|
||||
@@ -16,6 +16,7 @@ from fixtures.types import Operation, SigNoz, TestContainerDocker
|
||||
|
||||
V2_RULES_URL = "/api/v2/rules"
|
||||
V3_RULES_URL = "/api/v3/rules"
|
||||
RULE_VIEWS_URL = "/api/v2/rule_views"
|
||||
DOWNTIME_URL = "/api/v1/downtime_schedules"
|
||||
ROUTE_POLICIES_URL = "/api/v1/route_policies"
|
||||
|
||||
@@ -28,6 +29,7 @@ _ACTOR_PASSWORD = "password123Z$"
|
||||
_ACTOR_ROLE_NAME = "alert-authz-actor"
|
||||
|
||||
_TARGET_RULE = "alert authz target"
|
||||
_VIEW_NAME = "alert-authz-view"
|
||||
_DOWNTIME_NAME = "alert-authz-downtime"
|
||||
_ACTOR_DOWNTIME_NAME = "alert-authz-actor-downtime"
|
||||
_ROUTE_POLICY_NAME = "alert-authz-route-policy"
|
||||
@@ -400,6 +402,45 @@ def test_viewer_allowed_on_downtime_reads_and_forbidden_on_writes(
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
|
||||
def test_viewer_allowed_on_rule_views(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
token = get_token(_VIEWER_EMAIL, _VIEWER_PASSWORD)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(RULE_VIEWS_URL),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(RULE_VIEWS_URL),
|
||||
json={"name": _VIEW_NAME, "data": {"version": "v1", "sort": "updated_at", "order": "desc"}},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
view_id = response.json()["data"]["id"]
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"{RULE_VIEWS_URL}/{view_id}"),
|
||||
json={"name": _VIEW_NAME, "data": {"version": "v1", "sort": "updated_at", "order": "asc"}},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{RULE_VIEWS_URL}/{view_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
|
||||
def test_apply_license(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
@@ -506,6 +547,37 @@ def test_downtime_managed_role_tuples_enforced_with_license(
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, f"editor delete must pass on the migrated planned-maintenance delete tuple: {response.text}"
|
||||
|
||||
|
||||
def test_rule_view_managed_role_tuples_enforced_with_license(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
viewer_token = get_token(_VIEWER_EMAIL, _VIEWER_PASSWORD)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(RULE_VIEWS_URL),
|
||||
headers={"Authorization": f"Bearer {viewer_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, f"viewer list must pass on the migrated rule list tuple: {response.text}"
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(RULE_VIEWS_URL),
|
||||
json={"name": _VIEW_NAME, "data": {"version": "v1", "sort": "updated_at", "order": "desc"}},
|
||||
headers={"Authorization": f"Bearer {viewer_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, f"viewer create must pass, rule views are gated on rule list like dashboard views: {response.text}"
|
||||
view_id = response.json()["data"]["id"]
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{RULE_VIEWS_URL}/{view_id}"),
|
||||
headers={"Authorization": f"Bearer {viewer_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, f"viewer delete must pass on the migrated rule list tuple: {response.text}"
|
||||
|
||||
|
||||
def test_setup_channel_scoped_actor(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
@@ -630,6 +702,29 @@ def test_route_policy_channel_changes_follow_channel_attach_and_detach(
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
|
||||
def test_rule_views_forbidden_without_rule_list(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
token = get_token(_ACTOR_EMAIL, _ACTOR_PASSWORD)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(RULE_VIEWS_URL),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, response.text
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(RULE_VIEWS_URL),
|
||||
json={"name": _VIEW_NAME, "data": {"version": "v1", "sort": "updated_at", "order": "desc"}},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, response.text
|
||||
|
||||
|
||||
def test_downtime_naming_rule_forbidden_without_rule_attach(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
|
||||
Reference in New Issue
Block a user