mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-21 21:50:36 +01:00
Compare commits
2 Commits
fix/genera
...
ns/saved-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
735b9e7d68 | ||
|
|
29315d8c89 |
@@ -9,11 +9,6 @@ global:
|
||||
# the path component (e.g. /signoz in https://example.com/signoz) is used
|
||||
# as the base path for all HTTP routes (both API and web frontend).
|
||||
external_url: <unset>
|
||||
# origins (scheme://host[:port], no path) allowed as login redirect targets. include
|
||||
# the origin the signoz ui is served on. when not configured, redirect targets are
|
||||
# not validated.
|
||||
# allowed_origins:
|
||||
# - https://signoz.example.com
|
||||
# the url where the SigNoz backend receives telemetry data (traces, metrics, logs) from instrumented applications.
|
||||
ingestion_url: <unset>
|
||||
# the url of the SigNoz MCP server. when unset, the MCP settings page is hidden in the frontend.
|
||||
|
||||
@@ -7831,6 +7831,47 @@ components:
|
||||
enum:
|
||||
- basic
|
||||
type: string
|
||||
SavedviewtypesSavedView:
|
||||
properties:
|
||||
category:
|
||||
type: string
|
||||
compositeQuery:
|
||||
$ref: '#/components/schemas/V3CompositeQuery'
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
extraData:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
sourcePage:
|
||||
type: string
|
||||
tags:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- category
|
||||
- createdAt
|
||||
- createdBy
|
||||
- updatedAt
|
||||
- updatedBy
|
||||
- sourcePage
|
||||
- tags
|
||||
- compositeQuery
|
||||
- extraData
|
||||
type: object
|
||||
ServiceaccounttypesDeprecatedPostableServiceAccountRole:
|
||||
properties:
|
||||
id:
|
||||
@@ -8844,6 +8885,174 @@ components:
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
V3AttributeKey:
|
||||
properties:
|
||||
dataType:
|
||||
type: string
|
||||
isColumn:
|
||||
type: boolean
|
||||
isJSON:
|
||||
type: boolean
|
||||
key:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
V3BuilderQuery:
|
||||
properties:
|
||||
aggregateAttribute:
|
||||
$ref: '#/components/schemas/V3AttributeKey'
|
||||
aggregateOperator:
|
||||
type: string
|
||||
dataSource:
|
||||
type: string
|
||||
disabled:
|
||||
type: boolean
|
||||
expression:
|
||||
type: string
|
||||
filters:
|
||||
$ref: '#/components/schemas/V3FilterSet'
|
||||
functions:
|
||||
items:
|
||||
$ref: '#/components/schemas/V3Function'
|
||||
type: array
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/V3AttributeKey'
|
||||
type: array
|
||||
having:
|
||||
items:
|
||||
$ref: '#/components/schemas/V3Having'
|
||||
type: array
|
||||
legend:
|
||||
type: string
|
||||
limit:
|
||||
minimum: 0
|
||||
type: integer
|
||||
offset:
|
||||
minimum: 0
|
||||
type: integer
|
||||
orderBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/V3OrderBy'
|
||||
type: array
|
||||
pageSize:
|
||||
minimum: 0
|
||||
type: integer
|
||||
queryName:
|
||||
type: string
|
||||
reduceTo:
|
||||
type: string
|
||||
selectColumns:
|
||||
items:
|
||||
$ref: '#/components/schemas/V3AttributeKey'
|
||||
type: array
|
||||
seriesAggregation:
|
||||
type: string
|
||||
spaceAggregation:
|
||||
type: string
|
||||
stepInterval:
|
||||
format: int64
|
||||
type: integer
|
||||
temporality:
|
||||
type: string
|
||||
timeAggregation:
|
||||
type: string
|
||||
type: object
|
||||
V3ClickHouseQuery:
|
||||
properties:
|
||||
disabled:
|
||||
type: boolean
|
||||
legend:
|
||||
type: string
|
||||
query:
|
||||
type: string
|
||||
type: object
|
||||
V3CompositeQuery:
|
||||
properties:
|
||||
builderQueries:
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/V3BuilderQuery'
|
||||
type: object
|
||||
chQueries:
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/V3ClickHouseQuery'
|
||||
type: object
|
||||
fillGaps:
|
||||
type: boolean
|
||||
panelType:
|
||||
type: string
|
||||
promQueries:
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/V3PromQuery'
|
||||
type: object
|
||||
queries:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryEnvelope'
|
||||
type: array
|
||||
queryType:
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
required:
|
||||
- panelType
|
||||
- queryType
|
||||
type: object
|
||||
V3FilterItem:
|
||||
properties:
|
||||
key:
|
||||
$ref: '#/components/schemas/V3AttributeKey'
|
||||
op:
|
||||
type: string
|
||||
value: {}
|
||||
type: object
|
||||
V3FilterSet:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/components/schemas/V3FilterItem'
|
||||
nullable: true
|
||||
type: array
|
||||
op:
|
||||
type: string
|
||||
type: object
|
||||
V3Function:
|
||||
properties:
|
||||
args:
|
||||
items: {}
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
namedArgs:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
type: object
|
||||
V3Having:
|
||||
properties:
|
||||
columnName:
|
||||
type: string
|
||||
op:
|
||||
type: string
|
||||
value: {}
|
||||
type: object
|
||||
V3OrderBy:
|
||||
properties:
|
||||
columnName:
|
||||
type: string
|
||||
order:
|
||||
type: string
|
||||
type: object
|
||||
V3PromQuery:
|
||||
properties:
|
||||
disabled:
|
||||
type: boolean
|
||||
legend:
|
||||
type: string
|
||||
query:
|
||||
type: string
|
||||
stats:
|
||||
type: string
|
||||
type: object
|
||||
ZeustypesGettableHost:
|
||||
properties:
|
||||
hosts:
|
||||
@@ -22531,6 +22740,313 @@ paths:
|
||||
summary: Test alert rule
|
||||
tags:
|
||||
- rules
|
||||
/api/v2/saved_views:
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns saved views for the calling user's org, optionally filtered
|
||||
by source page, name, and category.
|
||||
operationId: ListSavedViews
|
||||
parameters:
|
||||
- in: query
|
||||
name: sourcePage
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: name
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: category
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedView'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: List saved views
|
||||
tags:
|
||||
- saved_view
|
||||
post:
|
||||
deprecated: false
|
||||
description: Persists a saved view for the explore page. Returns the id of the
|
||||
created view.
|
||||
operationId: CreateSavedView
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedView'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
nullable: true
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
summary: Create saved view
|
||||
tags:
|
||||
- saved_view
|
||||
/api/v2/saved_views/{viewId}:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: Deletes a saved view by id.
|
||||
operationId: DeleteSavedView
|
||||
parameters:
|
||||
- in: path
|
||||
name: viewId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
summary: Delete saved view
|
||||
tags:
|
||||
- saved_view
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns a saved view by id.
|
||||
operationId: GetSavedView
|
||||
parameters:
|
||||
- in: path
|
||||
name: viewId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedView'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Get saved view
|
||||
tags:
|
||||
- saved_view
|
||||
put:
|
||||
deprecated: false
|
||||
description: Replaces a saved view's name, tags, and query.
|
||||
operationId: UpdateSavedView
|
||||
parameters:
|
||||
- in: path
|
||||
name: viewId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedView'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedView'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
summary: Update saved view
|
||||
tags:
|
||||
- saved_view
|
||||
/api/v2/sessions:
|
||||
delete:
|
||||
deprecated: false
|
||||
|
||||
@@ -2,29 +2,6 @@ import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
// The wire shape these handlers can actually rely on. The generated
|
||||
// RenderErrorResponseDTO marks code/message/url/errors as required, but the
|
||||
// server omits any of them even on valid errors (e.g. a 400 with just a
|
||||
// message), so a present `error` object is all the guard can guarantee.
|
||||
type ErrorEnvelope = {
|
||||
error: {
|
||||
code?: string;
|
||||
message?: string;
|
||||
url?: string;
|
||||
errors?: { message?: string }[];
|
||||
};
|
||||
};
|
||||
|
||||
function isErrorEnvelope(data: unknown): data is ErrorEnvelope {
|
||||
return (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
'error' in data &&
|
||||
typeof (data as ErrorEnvelope).error === 'object' &&
|
||||
(data as ErrorEnvelope).error !== null
|
||||
);
|
||||
}
|
||||
|
||||
// @deprecated Use convertToApiError instead
|
||||
export function ErrorResponseHandlerForGeneratedAPIs(
|
||||
error: AxiosError<RenderErrorResponseDTO>,
|
||||
@@ -33,29 +10,15 @@ export function ErrorResponseHandlerForGeneratedAPIs(
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
if (response) {
|
||||
// The body isn't guaranteed to be an error envelope — e.g. a gateway 5xx
|
||||
// with an HTML/empty body during a deploy. Verify the shape before reading
|
||||
// it; otherwise synthesize a consistent error from the status.
|
||||
const data: unknown = response.data;
|
||||
if (isErrorEnvelope(data)) {
|
||||
const { code, message, url, errors } = data.error;
|
||||
throw new APIError({
|
||||
httpStatusCode: response.status || 500,
|
||||
error: {
|
||||
code: code ?? '',
|
||||
message: message ?? '',
|
||||
url: url ?? '',
|
||||
errors: (errors ?? []).map((e) => ({ message: e.message ?? '' })),
|
||||
},
|
||||
});
|
||||
}
|
||||
throw new APIError({
|
||||
httpStatusCode: response.status || 500,
|
||||
error: {
|
||||
code: 'UPSTREAM_UNAVAILABLE',
|
||||
message: error.message || 'Something went wrong',
|
||||
url: '',
|
||||
errors: [],
|
||||
code: response.data.error.code,
|
||||
message: response.data.error.message,
|
||||
url: response.data.error.url ?? '',
|
||||
errors: (response.data.error.errors ?? []).map((e) => ({
|
||||
message: e.message ?? '',
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -99,7 +62,9 @@ export function convertToApiError(
|
||||
return new APIError({
|
||||
httpStatusCode: response?.status || error.status || 500,
|
||||
error: {
|
||||
code: errorData?.code || 'UPSTREAM_UNAVAILABLE',
|
||||
code:
|
||||
errorData?.code ||
|
||||
String(response?.status || error.code || 'unknown_error'),
|
||||
message:
|
||||
errorData?.message ||
|
||||
response?.statusText ||
|
||||
|
||||
@@ -2,38 +2,19 @@ import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
function isErrorV2Resp(data: unknown): data is ErrorV2Resp {
|
||||
return (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
'error' in data &&
|
||||
typeof (data as ErrorV2Resp).error?.code === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
// reference - https://axios-http.com/docs/handling_errors
|
||||
export function ErrorResponseHandlerV2(error: AxiosError<ErrorV2Resp>): never {
|
||||
const { response, request } = error;
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
if (response) {
|
||||
// response.data isn't guaranteed to be a V2 envelope (e.g. a gateway 5xx
|
||||
// with an HTML/empty body during a deploy), so verify the shape first.
|
||||
const data: unknown = response.data;
|
||||
if (isErrorV2Resp(data)) {
|
||||
const { code, message, url, errors } = data.error;
|
||||
throw new APIError({
|
||||
httpStatusCode: response.status || 500,
|
||||
error: { code, message, url, errors },
|
||||
});
|
||||
}
|
||||
throw new APIError({
|
||||
httpStatusCode: response.status || 500,
|
||||
error: {
|
||||
code: 'UPSTREAM_UNAVAILABLE',
|
||||
message: error.message || 'Something went wrong',
|
||||
url: '',
|
||||
errors: [],
|
||||
code: response.data.error.code,
|
||||
message: response.data.error.message,
|
||||
url: response.data.error.url,
|
||||
errors: response.data.error.errors,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
function asAxiosError(partial: Partial<AxiosError>): AxiosError<ErrorV2Resp> {
|
||||
return partial as AxiosError<ErrorV2Resp>;
|
||||
}
|
||||
|
||||
// ErrorResponseHandlerV2 always throws — capture the APIError so assertions stay
|
||||
// unconditional.
|
||||
function runHandler(error: AxiosError<ErrorV2Resp>): APIError {
|
||||
try {
|
||||
ErrorResponseHandlerV2(error);
|
||||
} catch (thrown) {
|
||||
return thrown as APIError;
|
||||
}
|
||||
throw new Error('expected ErrorResponseHandlerV2 to throw');
|
||||
}
|
||||
|
||||
type ExpectedError = {
|
||||
httpStatusCode: number;
|
||||
code: string;
|
||||
message: string;
|
||||
errors: { message: string }[];
|
||||
};
|
||||
|
||||
// One row per response shape the handler must normalize. New shapes (with
|
||||
// different bodies) can be added here without a new test block.
|
||||
const cases: {
|
||||
name: string;
|
||||
error: AxiosError<ErrorV2Resp>;
|
||||
expected: ExpectedError;
|
||||
}[] = [
|
||||
{
|
||||
name: 'well-formed V2 error envelope',
|
||||
error: asAxiosError({
|
||||
message: 'Request failed with status code 400',
|
||||
response: {
|
||||
status: 400,
|
||||
data: {
|
||||
error: {
|
||||
code: 'bad_request',
|
||||
message: 'Invalid dashboard payload',
|
||||
url: 'https://signoz.io/docs',
|
||||
errors: [{ message: 'name is required' }, { message: 'name too long' }],
|
||||
},
|
||||
},
|
||||
} as AxiosError['response'],
|
||||
}),
|
||||
expected: {
|
||||
httpStatusCode: 400,
|
||||
code: 'bad_request',
|
||||
message: 'Invalid dashboard payload',
|
||||
errors: [{ message: 'name is required' }, { message: 'name too long' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Regression: during a deployment the gateway returns a 5xx with a
|
||||
// non-envelope body. Reading response.data.error.code used to throw a
|
||||
// TypeError from inside the handler itself. See engineering-pod#5760.
|
||||
name: '5xx with a non-envelope HTML body',
|
||||
error: asAxiosError({
|
||||
message: 'Request failed with status code 503',
|
||||
response: {
|
||||
status: 503,
|
||||
data: '<html><body>503 Service Temporarily Unavailable</body></html>',
|
||||
} as AxiosError['response'],
|
||||
}),
|
||||
expected: {
|
||||
httpStatusCode: 503,
|
||||
code: 'UPSTREAM_UNAVAILABLE',
|
||||
message: 'Request failed with status code 503',
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '5xx with an empty body',
|
||||
error: asAxiosError({
|
||||
message: 'Request failed with status code 502',
|
||||
response: {
|
||||
status: 502,
|
||||
data: undefined,
|
||||
} as AxiosError['response'],
|
||||
}),
|
||||
expected: {
|
||||
httpStatusCode: 502,
|
||||
code: 'UPSTREAM_UNAVAILABLE',
|
||||
message: 'Request failed with status code 502',
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'no response received (network error)',
|
||||
error: asAxiosError({
|
||||
message: 'Network Error',
|
||||
code: 'ERR_NETWORK',
|
||||
name: 'AxiosError',
|
||||
request: {},
|
||||
}),
|
||||
expected: {
|
||||
httpStatusCode: 500,
|
||||
code: 'ERR_NETWORK',
|
||||
message: 'Network Error',
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('ErrorResponseHandlerV2', () => {
|
||||
it.each(cases)(
|
||||
'normalizes $name into a consistent APIError',
|
||||
({ error, expected }) => {
|
||||
const apiError = runHandler(error);
|
||||
|
||||
expect(apiError).toBeInstanceOf(APIError);
|
||||
expect(apiError.getHttpStatusCode()).toBe(expected.httpStatusCode);
|
||||
expect(apiError.getErrorCode()).toBe(expected.code);
|
||||
expect(apiError.getErrorMessage()).toBe(expected.message);
|
||||
// The sub-error messages feed several parts of the UI, so assert them.
|
||||
expect(apiError.getErrorDetails().error.errors).toStrictEqual(
|
||||
expected.errors,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1,8 +1,6 @@
|
||||
import type {
|
||||
Querybuildertypesv5QueryWarnDataDTO as WarningDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { Querybuildertypesv5QueryWarnDataDTO as WarningDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
|
||||
import { panelStatusFromError, panelStatusFromWarning } from '../utils';
|
||||
@@ -46,7 +44,7 @@ describe('panelStatusFromError', () => {
|
||||
|
||||
it('falls back to the error message when there is no structured body', () => {
|
||||
expect(panelStatusFromError(new Error('boom'))).toStrictEqual({
|
||||
code: 'UPSTREAM_UNAVAILABLE',
|
||||
code: 'unknown_error',
|
||||
message: 'boom',
|
||||
docsUrl: undefined,
|
||||
messages: [],
|
||||
|
||||
@@ -42,12 +42,7 @@ export function toAPIError(
|
||||
try {
|
||||
ErrorResponseHandlerForGeneratedAPIs(error);
|
||||
} catch (apiError) {
|
||||
// UPSTREAM_UNAVAILABLE means the handler couldn't find a backend error code
|
||||
// (non-envelope body); prefer the caller's context-specific defaultMessage.
|
||||
if (
|
||||
apiError instanceof APIError &&
|
||||
apiError.getErrorCode() !== 'UPSTREAM_UNAVAILABLE'
|
||||
) {
|
||||
if (apiError instanceof APIError) {
|
||||
return apiError;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/promote"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/savedview"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/session"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
|
||||
@@ -75,6 +76,7 @@ type provider struct {
|
||||
rulerHandler ruler.Handler
|
||||
llmPricingRuleHandler llmpricingrule.Handler
|
||||
statsHandler statsreporter.Handler
|
||||
savedViewHandler savedview.Handler
|
||||
}
|
||||
|
||||
func NewFactory(
|
||||
@@ -110,6 +112,7 @@ func NewFactory(
|
||||
traceDetailHandler tracedetail.Handler,
|
||||
rulerHandler ruler.Handler,
|
||||
statsHandler statsreporter.Handler,
|
||||
savedViewHandler savedview.Handler,
|
||||
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
|
||||
return newProvider(
|
||||
@@ -148,6 +151,7 @@ func NewFactory(
|
||||
traceDetailHandler,
|
||||
rulerHandler,
|
||||
statsHandler,
|
||||
savedViewHandler,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -188,6 +192,7 @@ func newProvider(
|
||||
traceDetailHandler tracedetail.Handler,
|
||||
rulerHandler ruler.Handler,
|
||||
statsHandler statsreporter.Handler,
|
||||
savedViewHandler savedview.Handler,
|
||||
) (apiserver.APIServer, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
|
||||
router := mux.NewRouter().UseEncodedPath()
|
||||
@@ -227,6 +232,7 @@ func newProvider(
|
||||
rulerHandler: rulerHandler,
|
||||
llmPricingRuleHandler: llmPricingRuleHandler,
|
||||
statsHandler: statsHandler,
|
||||
savedViewHandler: savedViewHandler,
|
||||
}
|
||||
|
||||
provider.authzMiddleware = middleware.NewAuthZ(settings.Logger(), orgGetter, authzService)
|
||||
@@ -359,6 +365,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addSavedViewRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
101
pkg/apiserver/signozapiserver/savedview.go
Normal file
101
pkg/apiserver/signozapiserver/savedview.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addSavedViewRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/saved_views", handler.New(provider.authzMiddleware.ViewAccess(provider.savedViewHandler.List), handler.OpenAPIDef{
|
||||
ID: "ListSavedViews",
|
||||
Tags: []string{"saved_view"},
|
||||
Summary: "List saved views",
|
||||
Description: "Returns saved views for the calling user's org, optionally filtered by source page, name, and category.",
|
||||
Request: nil,
|
||||
RequestQuery: new(savedviewtypes.ListSavedViewsParams),
|
||||
RequestContentType: "",
|
||||
Response: new(savedviewtypes.SavedView),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/saved_views", handler.New(provider.authzMiddleware.EditAccess(provider.savedViewHandler.Create), handler.OpenAPIDef{
|
||||
ID: "CreateSavedView",
|
||||
Tags: []string{"saved_view"},
|
||||
Summary: "Create saved view",
|
||||
Description: "Persists a saved view for the explore page. Returns the id of the created view.",
|
||||
Request: new(savedviewtypes.SavedView),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(valuer.UUID),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/saved_views/{viewId}", handler.New(provider.authzMiddleware.ViewAccess(provider.savedViewHandler.Get), handler.OpenAPIDef{
|
||||
ID: "GetSavedView",
|
||||
Tags: []string{"saved_view"},
|
||||
Summary: "Get saved view",
|
||||
Description: "Returns a saved view by id.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(savedviewtypes.SavedView),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/saved_views/{viewId}", handler.New(provider.authzMiddleware.EditAccess(provider.savedViewHandler.Update), handler.OpenAPIDef{
|
||||
ID: "UpdateSavedView",
|
||||
Tags: []string{"saved_view"},
|
||||
Summary: "Update saved view",
|
||||
Description: "Replaces a saved view's name, tags, and query.",
|
||||
Request: new(savedviewtypes.SavedView),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(savedviewtypes.SavedView),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/saved_views/{viewId}", handler.New(provider.authzMiddleware.EditAccess(provider.savedViewHandler.Delete), handler.OpenAPIDef{
|
||||
ID: "DeleteSavedView",
|
||||
Tags: []string{"saved_view"},
|
||||
Summary: "Delete saved view",
|
||||
Description: "Deletes a saved view by id.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -12,15 +12,13 @@ import (
|
||||
|
||||
var (
|
||||
ErrCodeInvalidGlobalConfig = errors.MustNewCode("invalid_global_config")
|
||||
ErrCodeOriginNotAllowed = errors.MustNewCode("origin_not_allowed")
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ExternalURL *url.URL `mapstructure:"external_url"`
|
||||
AllowedOrigins []*url.URL `mapstructure:"allowed_origins"`
|
||||
IngestionURL *url.URL `mapstructure:"ingestion_url"`
|
||||
MCPURL *url.URL `mapstructure:"mcp_url"`
|
||||
AIAssistantURL *url.URL `mapstructure:"ai_assistant_url"`
|
||||
ExternalURL *url.URL `mapstructure:"external_url"`
|
||||
IngestionURL *url.URL `mapstructure:"ingestion_url"`
|
||||
MCPURL *url.URL `mapstructure:"mcp_url"`
|
||||
AIAssistantURL *url.URL `mapstructure:"ai_assistant_url"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
@@ -51,33 +49,9 @@ func (c Config) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
for _, origin := range c.AllowedOrigins {
|
||||
if origin == nil || origin.Scheme == "" || origin.Host == "" {
|
||||
return errors.NewInvalidInputf(ErrCodeInvalidGlobalConfig, "global::allowed_origins entries must be of the form scheme://host[:port], got %q", origin)
|
||||
}
|
||||
|
||||
if origin.Path != "" && origin.Path != "/" {
|
||||
return errors.NewInvalidInputf(ErrCodeInvalidGlobalConfig, "global::allowed_origins entries must not contain a path, got %q", origin)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Config) IsOriginAllowed(u *url.URL) bool {
|
||||
if len(c.AllowedOrigins) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, origin := range c.AllowedOrigins {
|
||||
if strings.EqualFold(origin.Scheme, u.Scheme) && strings.EqualFold(origin.Host, u.Host) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c Config) ExternalPath() string {
|
||||
if c.ExternalURL == nil || c.ExternalURL.Path == "" || c.ExternalURL.Path == "/" {
|
||||
return ""
|
||||
|
||||
@@ -123,26 +123,6 @@ func TestValidate(t *testing.T) {
|
||||
config: Config{ExternalURL: &url.URL{Path: "signoz"}},
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
name: "ValidAllowedOrigin",
|
||||
config: Config{AllowedOrigins: []*url.URL{{Scheme: "https", Host: "signoz.example.com"}}},
|
||||
fail: false,
|
||||
},
|
||||
{
|
||||
name: "AllowedOriginWithoutScheme",
|
||||
config: Config{AllowedOrigins: []*url.URL{{Host: "signoz.example.com"}}},
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
name: "AllowedOriginWithoutHost",
|
||||
config: Config{AllowedOrigins: []*url.URL{{Scheme: "https"}}},
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
name: "AllowedOriginWithPath",
|
||||
config: Config{AllowedOrigins: []*url.URL{{Scheme: "https", Host: "signoz.example.com", Path: "/login"}}},
|
||||
fail: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -157,96 +137,3 @@ func TestValidate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsOriginAllowedWhenUnconfigured(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
config Config
|
||||
}{
|
||||
{
|
||||
name: "Empty",
|
||||
config: Config{},
|
||||
},
|
||||
{
|
||||
name: "ExternalURLDoesNotActivateValidation",
|
||||
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "signoz.example.com"}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
u, err := url.Parse("https://anything.example.com/login")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, tc.config.IsOriginAllowed(u))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsOriginAllowed(t *testing.T) {
|
||||
config := Config{
|
||||
AllowedOrigins: []*url.URL{
|
||||
{Scheme: "https", Host: "signoz.example.com"},
|
||||
{Scheme: "http", Host: "localhost:3301"},
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "ConfiguredOrigin",
|
||||
input: "https://signoz.example.com/login",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "ConfiguredOriginWithQuery",
|
||||
input: "http://localhost:3301/login?next=/dashboards",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "CaseInsensitiveHost",
|
||||
input: "https://SigNoz.Example.Com/login",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "UnknownHost",
|
||||
input: "https://attacker.example.com/login",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "SchemeMismatch",
|
||||
input: "http://signoz.example.com/login",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "PortMismatch",
|
||||
input: "https://signoz.example.com:8443/login",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "SuffixConfusion",
|
||||
input: "https://evilsignoz.example.com/login",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "UserInfoConfusion",
|
||||
input: "https://signoz.example.com@attacker.example.com/login",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "SchemeRelative",
|
||||
input: "//attacker.example.com/login",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
u, err := url.Parse(tc.input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, config.IsOriginAllowed(u))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/savedview"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -33,7 +33,7 @@ func (handler *handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var view v3.SavedView
|
||||
var view savedviewtypes.SavedView
|
||||
if err := json.NewDecoder(r.Body).Decode(&view); err != nil {
|
||||
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode request body"))
|
||||
return
|
||||
@@ -95,7 +95,7 @@ func (handler *handler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse view id"))
|
||||
return
|
||||
}
|
||||
var view v3.SavedView
|
||||
var view savedviewtypes.SavedView
|
||||
if err := json.NewDecoder(r.Body).Decode(&view); err != nil {
|
||||
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode request body"))
|
||||
return
|
||||
|
||||
@@ -24,8 +24,8 @@ func NewModule(sqlstore sqlstore.SQLStore) savedview.Module {
|
||||
return &module{sqlstore: sqlstore}
|
||||
}
|
||||
|
||||
func (module *module) GetViewsForFilters(ctx context.Context, orgID string, sourcePage string, name string, category string) ([]*v3.SavedView, error) {
|
||||
var views []savedviewtypes.SavedView
|
||||
func (module *module) GetViewsForFilters(ctx context.Context, orgID string, sourcePage string, name string, category string) ([]*savedviewtypes.SavedView, error) {
|
||||
var views []savedviewtypes.StorableSavedView
|
||||
var err error
|
||||
if len(category) == 0 {
|
||||
err = module.sqlstore.BunDB().NewSelect().Model(&views).Where("org_id = ? AND source_page = ? AND name LIKE ?", orgID, sourcePage, "%"+name+"%").Scan(ctx)
|
||||
@@ -36,14 +36,14 @@ func (module *module) GetViewsForFilters(ctx context.Context, orgID string, sour
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in getting saved views")
|
||||
}
|
||||
|
||||
var savedViews []*v3.SavedView
|
||||
var savedViews []*savedviewtypes.SavedView
|
||||
for _, view := range views {
|
||||
var compositeQuery v3.CompositeQuery
|
||||
err = json.Unmarshal([]byte(view.Data), &compositeQuery)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in unmarshalling explorer query data: %s", err.Error())
|
||||
}
|
||||
savedViews = append(savedViews, &v3.SavedView{
|
||||
savedViews = append(savedViews, &savedviewtypes.SavedView{
|
||||
ID: view.ID,
|
||||
Name: view.Name,
|
||||
CreatedAt: view.CreatedAt,
|
||||
@@ -59,7 +59,7 @@ func (module *module) GetViewsForFilters(ctx context.Context, orgID string, sour
|
||||
return savedViews, nil
|
||||
}
|
||||
|
||||
func (module *module) CreateView(ctx context.Context, orgID string, view v3.SavedView) (valuer.UUID, error) {
|
||||
func (module *module) CreateView(ctx context.Context, orgID string, view savedviewtypes.SavedView) (valuer.UUID, error) {
|
||||
data, err := json.Marshal(view.CompositeQuery)
|
||||
if err != nil {
|
||||
return valuer.UUID{}, errors.WrapInternalf(err, errors.CodeInternal, "error in marshalling explorer query data")
|
||||
@@ -77,7 +77,7 @@ func (module *module) CreateView(ctx context.Context, orgID string, view v3.Save
|
||||
createBy := claims.Email
|
||||
updatedBy := claims.Email
|
||||
|
||||
dbView := savedviewtypes.SavedView{
|
||||
dbView := savedviewtypes.StorableSavedView{
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
@@ -105,8 +105,8 @@ func (module *module) CreateView(ctx context.Context, orgID string, view v3.Save
|
||||
return uuid, nil
|
||||
}
|
||||
|
||||
func (module *module) GetView(ctx context.Context, orgID string, uuid valuer.UUID) (*v3.SavedView, error) {
|
||||
var view savedviewtypes.SavedView
|
||||
func (module *module) GetView(ctx context.Context, orgID string, uuid valuer.UUID) (*savedviewtypes.SavedView, error) {
|
||||
var view savedviewtypes.StorableSavedView
|
||||
err := module.sqlstore.BunDB().NewSelect().Model(&view).Where("org_id = ? AND id = ?", orgID, uuid.StringValue()).Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in getting saved view")
|
||||
@@ -117,7 +117,7 @@ func (module *module) GetView(ctx context.Context, orgID string, uuid valuer.UUI
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in unmarshalling explorer query data")
|
||||
}
|
||||
return &v3.SavedView{
|
||||
return &savedviewtypes.SavedView{
|
||||
ID: view.ID,
|
||||
Name: view.Name,
|
||||
Category: view.Category,
|
||||
@@ -132,7 +132,7 @@ func (module *module) GetView(ctx context.Context, orgID string, uuid valuer.UUI
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (module *module) UpdateView(ctx context.Context, orgID string, uuid valuer.UUID, view v3.SavedView) error {
|
||||
func (module *module) UpdateView(ctx context.Context, orgID string, uuid valuer.UUID, view savedviewtypes.SavedView) error {
|
||||
data, err := json.Marshal(view.CompositeQuery)
|
||||
if err != nil {
|
||||
return errors.WrapInternalf(err, errors.CodeInternal, "error in marshalling explorer query data")
|
||||
@@ -147,7 +147,7 @@ func (module *module) UpdateView(ctx context.Context, orgID string, uuid valuer.
|
||||
updatedBy := claims.Email
|
||||
|
||||
_, err = module.sqlstore.BunDB().NewUpdate().
|
||||
Model(&savedviewtypes.SavedView{}).
|
||||
Model(&savedviewtypes.StorableSavedView{}).
|
||||
Set("updated_at = ?, updated_by = ?, name = ?, category = ?, source_page = ?, tags = ?, data = ?, extra_data = ?",
|
||||
updatedAt, updatedBy, view.Name, view.Category, view.SourcePage, strings.Join(view.Tags, ","), data, view.ExtraData).
|
||||
Where("id = ?", uuid.StringValue()).
|
||||
@@ -161,7 +161,7 @@ func (module *module) UpdateView(ctx context.Context, orgID string, uuid valuer.
|
||||
|
||||
func (module *module) DeleteView(ctx context.Context, orgID string, uuid valuer.UUID) error {
|
||||
_, err := module.sqlstore.BunDB().NewDelete().
|
||||
Model(&savedviewtypes.SavedView{}).
|
||||
Model(&savedviewtypes.StorableSavedView{}).
|
||||
Where("id = ?", uuid.StringValue()).
|
||||
Where("org_id = ?", orgID).
|
||||
Exec(ctx)
|
||||
@@ -172,7 +172,7 @@ func (module *module) DeleteView(ctx context.Context, orgID string, uuid valuer.
|
||||
}
|
||||
|
||||
func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
|
||||
savedViews := []*savedviewtypes.SavedView{}
|
||||
savedViews := []*savedviewtypes.StorableSavedView{}
|
||||
|
||||
err := module.
|
||||
sqlstore.
|
||||
|
||||
@@ -4,19 +4,19 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/statsreporter"
|
||||
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Module interface {
|
||||
GetViewsForFilters(ctx context.Context, orgID string, sourcePage string, name string, category string) ([]*v3.SavedView, error)
|
||||
GetViewsForFilters(ctx context.Context, orgID string, sourcePage string, name string, category string) ([]*savedviewtypes.SavedView, error)
|
||||
|
||||
CreateView(ctx context.Context, orgID string, view v3.SavedView) (valuer.UUID, error)
|
||||
CreateView(ctx context.Context, orgID string, view savedviewtypes.SavedView) (valuer.UUID, error)
|
||||
|
||||
GetView(ctx context.Context, orgID string, uuid valuer.UUID) (*v3.SavedView, error)
|
||||
GetView(ctx context.Context, orgID string, uuid valuer.UUID) (*savedviewtypes.SavedView, error)
|
||||
|
||||
UpdateView(ctx context.Context, orgID string, uuid valuer.UUID, view v3.SavedView) error
|
||||
UpdateView(ctx context.Context, orgID string, uuid valuer.UUID, view savedviewtypes.SavedView) error
|
||||
|
||||
DeleteView(ctx context.Context, orgID string, uuid valuer.UUID) error
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/session"
|
||||
@@ -24,28 +23,26 @@ import (
|
||||
)
|
||||
|
||||
type module struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
authNs map[authtypes.AuthNProvider]authn.AuthN
|
||||
userSetter user.Setter
|
||||
userGetter user.Getter
|
||||
authDomain authdomain.Module
|
||||
tokenizer tokenizer.Tokenizer
|
||||
orgGetter organization.Getter
|
||||
authz authz.AuthZ
|
||||
globalConfig global.Config
|
||||
settings factory.ScopedProviderSettings
|
||||
authNs map[authtypes.AuthNProvider]authn.AuthN
|
||||
userSetter user.Setter
|
||||
userGetter user.Getter
|
||||
authDomain authdomain.Module
|
||||
tokenizer tokenizer.Tokenizer
|
||||
orgGetter organization.Getter
|
||||
authz authz.AuthZ
|
||||
}
|
||||
|
||||
func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.AuthNProvider]authn.AuthN, userSetter user.Setter, userGetter user.Getter, authDomain authdomain.Module, tokenizer tokenizer.Tokenizer, orgGetter organization.Getter, authz authz.AuthZ, globalConfig global.Config) session.Module {
|
||||
func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.AuthNProvider]authn.AuthN, userSetter user.Setter, userGetter user.Getter, authDomain authdomain.Module, tokenizer tokenizer.Tokenizer, orgGetter organization.Getter, authz authz.AuthZ) session.Module {
|
||||
return &module{
|
||||
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/session/implsession"),
|
||||
authNs: authNs,
|
||||
userSetter: userSetter,
|
||||
userGetter: userGetter,
|
||||
authDomain: authDomain,
|
||||
tokenizer: tokenizer,
|
||||
orgGetter: orgGetter,
|
||||
authz: authz,
|
||||
globalConfig: globalConfig,
|
||||
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/session/implsession"),
|
||||
authNs: authNs,
|
||||
userSetter: userSetter,
|
||||
userGetter: userGetter,
|
||||
authDomain: authDomain,
|
||||
tokenizer: tokenizer,
|
||||
orgGetter: orgGetter,
|
||||
authz: authz,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,10 +140,6 @@ func (module *module) CreateCallbackAuthNSession(ctx context.Context, authNProvi
|
||||
return "", err
|
||||
}
|
||||
|
||||
if callbackIdentity.State.URL.Host != "" && !module.globalConfig.IsOriginAllowed(callbackIdentity.State.URL) {
|
||||
return "", errors.Newf(errors.TypeForbidden, global.ErrCodeOriginNotAllowed, "state redirect %q is not an allowed origin", callbackIdentity.State.URL.String())
|
||||
}
|
||||
|
||||
authDomain, err := module.authDomain.GetByOrgIDAndID(ctx, callbackIdentity.OrgID, callbackIdentity.State.DomainID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -224,10 +217,6 @@ func (module *module) getOrgSessionContext(ctx context.Context, org *types.Organ
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !module.globalConfig.IsOriginAllowed(siteURL) {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, global.ErrCodeOriginNotAllowed, "ref %q is not an allowed origin", siteURL.String())
|
||||
}
|
||||
|
||||
loginURL, err := provider.LoginURL(ctx, siteURL, authDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
signozerrors "github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
)
|
||||
@@ -519,8 +518,8 @@ type CompositeQuery struct {
|
||||
|
||||
Queries []qbtypes.QueryEnvelope `json:"queries,omitempty"`
|
||||
|
||||
PanelType PanelType `json:"panelType"`
|
||||
QueryType QueryType `json:"queryType"`
|
||||
PanelType PanelType `json:"panelType" required:"true"`
|
||||
QueryType QueryType `json:"queryType" required:"true"`
|
||||
// Unit for the time series data shown in the graph
|
||||
// This is used in alerts to format the value and threshold
|
||||
Unit string `json:"unit,omitempty"`
|
||||
@@ -1423,34 +1422,6 @@ func (p *Point) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// SavedView is a saved query for the explore page
|
||||
// It is a composite query with a source page name and user defined tags
|
||||
// The source page name is used to identify the page that initiated the query
|
||||
// The source page could be "traces", "logs", "metrics".
|
||||
type SavedView struct {
|
||||
ID valuer.UUID `json:"id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
SourcePage string `json:"sourcePage"`
|
||||
Tags []string `json:"tags"`
|
||||
CompositeQuery *CompositeQuery `json:"compositeQuery"`
|
||||
// ExtraData is JSON encoded data used by frontend to store additional data
|
||||
ExtraData string `json:"extraData"`
|
||||
}
|
||||
|
||||
func (eq *SavedView) Validate() error {
|
||||
|
||||
if eq.CompositeQuery == nil {
|
||||
return fmt.Errorf("composite query is required")
|
||||
}
|
||||
|
||||
return eq.CompositeQuery.Validate()
|
||||
}
|
||||
|
||||
type LatencyMetricMetadataResponse struct {
|
||||
Delta bool `json:"delta"`
|
||||
Le []float64 `json:"le"`
|
||||
|
||||
@@ -149,7 +149,7 @@ func NewModules(
|
||||
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
|
||||
RawDataExport: implrawdataexport.NewModule(querier),
|
||||
AuthDomain: authDomainModule,
|
||||
Session: implsession.NewModule(providerSettings, authNs, userSetter, userGetter, authDomainModule, tokenizer, orgGetter, authz, config.Global),
|
||||
Session: implsession.NewModule(providerSettings, authNs, userSetter, userGetter, authDomainModule, tokenizer, orgGetter, authz),
|
||||
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
|
||||
Services: implservices.NewModule(querier, telemetryStore),
|
||||
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, fl, providerSettings, config.MetricsExplorer),
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/promote"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/savedview"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/session"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
|
||||
@@ -88,6 +89,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ tracedetail.Handler }{},
|
||||
struct{ ruler.Handler }{},
|
||||
struct{ statsreporter.Handler }{},
|
||||
struct{ savedview.Handler }{},
|
||||
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -320,6 +320,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
handlers.TraceDetail,
|
||||
handlers.RulerHandler,
|
||||
handlers.StatsHandler,
|
||||
handlers.SavedView,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
40
pkg/types/savedviewtypes/domain.go
Normal file
40
pkg/types/savedviewtypes/domain.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package savedviewtypes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// SavedView is a saved query for the explore page. It is a composite query
|
||||
// with a source page name and user defined tags. The source page name is
|
||||
// used to identify the page that initiated the query. The source page
|
||||
// could be "traces", "logs", "metrics".
|
||||
//
|
||||
// CompositeQuery already carries both the legacy builderQueries/chQueries/
|
||||
// promQueries shape and the v5 Queries envelope (each field omitempty), so
|
||||
// this same type serves both /api/v1/explorer/views and /api/v2/saved_views.
|
||||
type SavedView struct {
|
||||
ID valuer.UUID `json:"id,omitempty"`
|
||||
Name string `json:"name" required:"true"`
|
||||
Category string `json:"category" required:"true"`
|
||||
CreatedAt time.Time `json:"createdAt" required:"true"`
|
||||
CreatedBy string `json:"createdBy" required:"true"`
|
||||
UpdatedAt time.Time `json:"updatedAt" required:"true"`
|
||||
UpdatedBy string `json:"updatedBy" required:"true"`
|
||||
SourcePage string `json:"sourcePage" required:"true"`
|
||||
Tags []string `json:"tags" required:"true" nullable:"true"`
|
||||
CompositeQuery *v3.CompositeQuery `json:"compositeQuery" required:"true"`
|
||||
// ExtraData is JSON encoded data used by frontend to store additional data
|
||||
ExtraData string `json:"extraData" required:"true"`
|
||||
}
|
||||
|
||||
func (eq *SavedView) Validate() error {
|
||||
if eq.CompositeQuery == nil {
|
||||
return fmt.Errorf("composite query is required")
|
||||
}
|
||||
|
||||
return eq.CompositeQuery.Validate()
|
||||
}
|
||||
11
pkg/types/savedviewtypes/list.go
Normal file
11
pkg/types/savedviewtypes/list.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package savedviewtypes
|
||||
|
||||
// ListSavedViewsParams describes the query params accepted by the saved
|
||||
// views list endpoint. It exists purely to document the endpoint's query
|
||||
// params in the generated OpenAPI spec -- the handler parses these directly
|
||||
// off the request URL.
|
||||
type ListSavedViewsParams struct {
|
||||
SourcePage string `query:"sourcePage"`
|
||||
Name string `query:"name"`
|
||||
Category string `query:"category"`
|
||||
}
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type SavedView struct {
|
||||
// StorableSavedView is the bun-persisted row shape for the saved_views
|
||||
// table. CompositeQuery is stored JSON-encoded in Data.
|
||||
type StorableSavedView struct {
|
||||
bun.BaseModel `bun:"table:saved_views"`
|
||||
|
||||
types.Identifiable
|
||||
@@ -22,7 +24,7 @@ type SavedView struct {
|
||||
ExtraData string `json:"extraData" bun:"extra_data,type:text"`
|
||||
}
|
||||
|
||||
func NewStatsFromSavedViews(savedViews []*SavedView) map[string]any {
|
||||
func NewStatsFromSavedViews(savedViews []*StorableSavedView) map[string]any {
|
||||
stats := make(map[string]any)
|
||||
for _, savedView := range savedViews {
|
||||
key := "savedview.source." + strings.ToLower(string(savedView.SourcePage)) + ".count"
|
||||
|
||||
Reference in New Issue
Block a user