Compare commits

..

8 Commits

Author SHA1 Message Date
Karan Balani
cc85cf19af Merge branch 'fix/required_fields_1' of github.com:SigNoz/signoz into fix/required_fields_1 2026-02-20 01:02:35 +05:30
Karan Balani
f257f784c5 chore: add required for gettablehost also 2026-02-20 01:01:57 +05:30
Karan Balani
6213dad1ef Merge branch 'main' into fix/required_fields_1 2026-02-20 00:48:31 +05:30
Karan Balani
4433990855 fix: add required tags wherever needed 2026-02-19 23:04:32 +05:30
Srikanth Chekuri
2b929421a1 chore: add notes about struct tags for better openapi integration (#10365) 2026-02-19 22:32:44 +05:30
Nityananda Gohain
2792e20aa2 fix: instrumentation changes to capture query duration properly (#10362)
* fix: instrumentation changes to capture query duration properly

* chore: move changes to rows.go
2026-02-19 16:02:16 +00:00
Karan Balani
473be1b174 feat: add zeus handler with profile and host apis (#10336)
### 📄 Summary
- Expose Zeus PutProfile, PutHost and GetHost APIs as first-class OpenAPI-spec endpoints, replacing the previous proxy-based approach
- Introduce typed request structs (PostableProfile, PostableHost) instead of raw []byte for type safety and OpenAPI documentation
- Wire Zeus handler through the standard dependency chain: handler interface, handler implementation, Handlers struct, signozapiserver provider

#### Changes
- PUT /api/v2/zeus/profiles - saves deployment profile to Zeus
- PUT /api/v2/zeus/hosts - saves deployment host to Zeus
- GET /api/v2/zeus/hosts - gets the deployment host from Zeus
- All the above new APIs need Admin access

Also:
- httpzeus provider — marshaling now happens in the provider; upstream error messages are passed through instead of being swallowed; fixes wrong upstream path (/hosts → /host); adds 409 Conflict mapping; replaces errors.Newf with errors.New

#### Issues closed by this PR
Closes https://github.com/SigNoz/platform-pod/issues/1722
2026-02-19 15:40:37 +00:00
Ashwin Bhatkal
6d0c13f9a7 fix: dynamic variables options load first time (#10361)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-02-19 20:25:19 +05:30
24 changed files with 1359 additions and 132 deletions

View File

@@ -307,11 +307,16 @@ components:
type: string
value:
type: string
required:
- id
- value
type: object
GatewaytypesGettableCreatedIngestionKeyLimit:
properties:
id:
type: string
required:
- id
type: object
GatewaytypesGettableIngestionKeys:
properties:
@@ -432,6 +437,8 @@ components:
type: string
nullable: true
type: array
required:
- name
type: object
GatewaytypesPostableIngestionKeyLimit:
properties:
@@ -454,6 +461,8 @@ components:
type: string
nullable: true
type: array
required:
- config
type: object
MetricsexplorertypesListMetric:
properties:
@@ -1920,6 +1929,82 @@ components:
format: date-time
type: string
type: object
ZeustypesGettableHost:
properties:
hosts:
items:
$ref: '#/components/schemas/ZeustypesHost'
nullable: true
type: array
name:
type: string
state:
type: string
tier:
type: string
required:
- name
- state
- tier
- hosts
type: object
ZeustypesHost:
properties:
is_default:
type: boolean
name:
type: string
url:
type: string
required:
- name
- is_default
- url
type: object
ZeustypesPostableHost:
properties:
name:
type: string
required:
- name
type: object
ZeustypesPostableProfile:
properties:
existing_observability_tool:
type: string
has_existing_observability_tool:
type: boolean
logs_scale_per_day_in_gb:
format: int64
type: integer
number_of_hosts:
format: int64
type: integer
number_of_services:
format: int64
type: integer
reasons_for_interest_in_signoz:
items:
type: string
nullable: true
type: array
timeline_for_migrating_to_signoz:
type: string
uses_otel:
type: boolean
where_did_you_discover_signoz:
type: string
required:
- uses_otel
- has_existing_observability_tool
- existing_observability_tool
- reasons_for_interest_in_signoz
- logs_scale_per_day_in_gb
- number_of_services
- number_of_hosts
- where_did_you_discover_signoz
- timeline_for_migrating_to_signoz
type: object
securitySchemes:
api_key:
description: API Keys
@@ -4720,6 +4805,7 @@ paths:
parameters:
- in: query
name: name
required: true
schema:
type: string
- in: query
@@ -5538,6 +5624,174 @@ paths:
summary: Rotate session
tags:
- sessions
/api/v2/zeus/hosts:
get:
deprecated: false
description: This endpoint gets the host info from zeus.
operationId: GetHosts
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/ZeustypesGettableHost'
status:
type: string
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:
- ADMIN
- tokenizer:
- ADMIN
summary: Get host info from Zeus.
tags:
- zeus
put:
deprecated: false
description: This endpoint saves the host of a deployment to zeus.
operationId: PutHost
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ZeustypesPostableHost'
responses:
"204":
description: No Content
"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
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Put host in Zeus for a deployment.
tags:
- zeus
/api/v2/zeus/profiles:
put:
deprecated: false
description: This endpoint saves the profile of a deployment to zeus.
operationId: PutProfile
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ZeustypesPostableProfile'
responses:
"204":
description: No Content
"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
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Put profile in Zeus for a deployment.
tags:
- zeus
/api/v5/query_range:
post:
deprecated: false

View File

@@ -155,6 +155,7 @@ The `handler.New` function ties the HTTP handler to OpenAPI metadata via `OpenAP
- **Request / RequestContentType**:
- `Request` is a Go type that describes the request body or form.
- `RequestContentType` is usually `"application/json"` or `"application/x-www-form-urlencoded"` (for callbacks like SAML).
- **RequestExamples**: An array of `handler.OpenAPIExample` that provide concrete request payloads in the generated spec. See [Adding request examples](#adding-request-examples) below.
- **Response / ResponseContentType**:
- `Response` is the Go type for the successful response payload.
- `ResponseContentType` is usually `"application/json"`; use `""` for responses without a body.
@@ -172,8 +173,169 @@ See existing examples in:
- `addUserRoutes` (for typical JSON request/response)
- `addSessionRoutes` (for form-encoded and redirect flows)
## OpenAPI schema details for request/response types
The OpenAPI spec is generated from the Go types you pass as `Request` and `Response` in `OpenAPIDef`. The following struct tags and interfaces control how those types appear in the generated schema.
### Adding request examples
Use the `RequestExamples` field in `OpenAPIDef` to provide concrete request payloads. Each example is a `handler.OpenAPIExample`:
```go
type OpenAPIExample struct {
Name string // unique key for the example (e.g. "traces_time_series")
Summary string // short description shown in docs (e.g. "Time series: count spans grouped by service")
Description string // optional longer description
Value any // the example payload, typically map[string]any
}
```
For reference, see `pkg/querier/openapi.go` which defines some examples for the `/api/v5/query_range` endpoint:
```go
var QueryRangeV5OpenAPIDef = handler.OpenAPIDef{
ID: "QueryRangeV5",
Tags: []string{"query"},
Summary: "Query range",
Description: "Execute a composite query over a time range.",
Request: new(qbtypes.QueryRangeRequest),
RequestExamples: queryRangeV5Examples, // []handler.OpenAPIExample
// ...
}
var queryRangeV5Examples = []handler.OpenAPIExample{
{
Name: "traces_time_series",
Summary: "Time series: count spans grouped by service",
Value: map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "time_series",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "traces",
// ...
},
},
},
},
},
},
// ... more examples
}
```
### `required` tag
Use `required:"true"` on struct fields where the property is expected to be **present** in the JSON payload. This is different from the zero value, a field can have its zero value (e.g. `0`, `""`, `false`) and still be required. The `required` tag means the key itself must exist in the JSON object.
```go
type ListItem struct {
...
}
type ListResponse struct {
List []ListItem `json:"list" required:"true" nullable:"true"`
Total uint64 `json:"total" required:"true"`
}
```
In this example, a response like `{"list": null, "total": 0}` is valid. Both keys are present (satisfying `required`), `total` has its zero value, and `list` is null (allowed by `nullable`). But `{"total": 0}` would violate the schema because the `list` key is missing.
### `nullable` tag
Use `nullable:"true"` on struct fields that can be `null` in the JSON payload. This is especially important for **slice and map fields** because in Go, the zero value for these types is `nil`, which serializes to `null` in JSON (not `[]` or `{}`).
Be explicit about the distinction:
- **Nullable list** (`nullable:"true"`): the field can be `null`. Use this when the Go code may return `nil` for the slice.
- **Non-nullable list** (no `nullable` tag): the field is always an array, never `null`. Ensure the Go code initializes it to an empty slice (e.g. `make([]T, 0)`) before serializing.
```go
// Non-nullable: Go code must ensure this is always an initialized slice.
type NonNullableExample struct {
Items []Item `json:"items" required:"true"`
}
```
When defining your types, ask yourself: "Can this field be `null` in the JSON response, or is it always an array/object?" If the Go code ever returns a `nil` slice or map, mark it `nullable:"true"`.
### `Enum()` method
For types that have a fixed set of acceptable values, implement the `Enum() []any` method. This generates an `enum` constraint in the JSON schema so the OpenAPI spec accurately restricts the values.
```go
type Signal struct {
valuer.String
}
var (
SignalTraces = Signal{valuer.NewString("traces")}
SignalLogs = Signal{valuer.NewString("logs")}
SignalMetrics = Signal{valuer.NewString("metrics")}
)
func (Signal) Enum() []any {
return []any{
SignalTraces,
SignalLogs,
SignalMetrics,
}
}
```
This produces the following in the generated OpenAPI spec:
```yaml
Signal:
enum:
- traces
- logs
- metrics
type: string
```
Every type with a known set of values **must** implement `Enum()`. Without it, the JSON schema will only show the base type (e.g. `string`) with no value constraints.
### `JSONSchema()` method (custom schema)
For types that need a completely custom JSON schema (for example, a field that accepts either a string or a number), implement the `jsonschema.Exposer` interface:
```go
var _ jsonschema.Exposer = Step{}
func (Step) JSONSchema() (jsonschema.Schema, error) {
s := jsonschema.Schema{}
s.WithDescription("Step interval. Accepts a duration string or seconds.")
strSchema := jsonschema.Schema{}
strSchema.WithType(jsonschema.String.Type())
strSchema.WithExamples("60s", "5m", "1h")
numSchema := jsonschema.Schema{}
numSchema.WithType(jsonschema.Number.Type())
numSchema.WithExamples(60, 300, 3600)
s.OneOf = []jsonschema.SchemaOrBool{
strSchema.ToSchemaOrBool(),
numSchema.ToSchemaOrBool(),
}
return s, nil
}
```
## What should I remember?
- **Keep handlers thin**: focus on HTTP concerns and delegate logic to modules/services.
- **Always register routes through `signozapiserver`** using `handler.New` and a complete `OpenAPIDef`.
- **Choose accurate request/response types** from the `types` packages so OpenAPI schemas are correct.
- **Add `required:"true"`** on fields where the key must be present in the JSON (this is about key presence, not about the zero value).
- **Add `nullable:"true"`** on fields that can be `null`. Pay special attention to slices and maps -- in Go these default to `nil` which serializes to `null`. If the field should always be an array, initialize it and do not mark it nullable.
- **Implement `Enum()`** on every type that has a fixed set of acceptable values so the JSON schema generates proper `enum` constraints.
- **Add request examples** via `RequestExamples` in `OpenAPIDef` for any non-trivial endpoint. See `pkg/querier/openapi.go` for reference.

View File

@@ -3,6 +3,7 @@ package httpzeus
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/url"
@@ -10,6 +11,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/http/client"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
"github.com/SigNoz/signoz/pkg/zeus"
"github.com/tidwall/gjson"
)
@@ -119,8 +121,13 @@ func (provider *Provider) PutMeters(ctx context.Context, key string, data []byte
return err
}
func (provider *Provider) PutProfile(ctx context.Context, key string, body []byte) error {
_, err := provider.do(
func (provider *Provider) PutProfile(ctx context.Context, key string, profile *zeustypes.PostableProfile) error {
body, err := json.Marshal(profile)
if err != nil {
return err
}
_, err = provider.do(
ctx,
provider.config.URL.JoinPath("/v2/profiles/me"),
http.MethodPut,
@@ -131,10 +138,15 @@ func (provider *Provider) PutProfile(ctx context.Context, key string, body []byt
return err
}
func (provider *Provider) PutHost(ctx context.Context, key string, body []byte) error {
_, err := provider.do(
func (provider *Provider) PutHost(ctx context.Context, key string, host *zeustypes.PostableHost) error {
body, err := json.Marshal(host)
if err != nil {
return err
}
_, err = provider.do(
ctx,
provider.config.URL.JoinPath("/v2/deployments/me/hosts"),
provider.config.URL.JoinPath("/v2/deployments/me/host"),
http.MethodPut,
key,
body,
@@ -169,21 +181,28 @@ func (provider *Provider) do(ctx context.Context, url *url.URL, method string, k
return body, nil
}
return nil, provider.errFromStatusCode(response.StatusCode)
errorMessage := gjson.GetBytes(body, "error").String()
if errorMessage == "" {
errorMessage = "an unknown error occurred"
}
return nil, provider.errFromStatusCode(response.StatusCode, errorMessage)
}
// This can be taken down to the client package
func (provider *Provider) errFromStatusCode(statusCode int) error {
func (provider *Provider) errFromStatusCode(statusCode int, errorMessage string) error {
switch statusCode {
case http.StatusBadRequest:
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "bad request")
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, errorMessage)
case http.StatusUnauthorized:
return errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated")
return errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, errorMessage)
case http.StatusForbidden:
return errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "forbidden")
return errors.New(errors.TypeForbidden, errors.CodeForbidden, errorMessage)
case http.StatusNotFound:
return errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "not found")
return errors.New(errors.TypeNotFound, errors.CodeNotFound, errorMessage)
case http.StatusConflict:
return errors.New(errors.TypeAlreadyExists, errors.CodeAlreadyExists, errorMessage)
}
return errors.Newf(errors.TypeInternal, errors.CodeInternal, "internal")
return errors.New(errors.TypeInternal, errors.CodeInternal, errorMessage)
}

View File

@@ -678,7 +678,7 @@ export const useUpdateIngestionKeyLimit = <
* @summary Search ingestion keys for workspace
*/
export const searchIngestionKeys = (
params?: SearchIngestionKeysParams,
params: SearchIngestionKeysParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<SearchIngestionKeys200>({
@@ -699,7 +699,7 @@ export const getSearchIngestionKeysQueryOptions = <
TData = Awaited<ReturnType<typeof searchIngestionKeys>>,
TError = RenderErrorResponseDTO
>(
params?: SearchIngestionKeysParams,
params: SearchIngestionKeysParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof searchIngestionKeys>>,
@@ -737,7 +737,7 @@ export function useSearchIngestionKeys<
TData = Awaited<ReturnType<typeof searchIngestionKeys>>,
TError = RenderErrorResponseDTO
>(
params?: SearchIngestionKeysParams,
params: SearchIngestionKeysParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof searchIngestionKeys>>,
@@ -762,7 +762,7 @@ export function useSearchIngestionKeys<
*/
export const invalidateSearchIngestionKeys = async (
queryClient: QueryClient,
params?: SearchIngestionKeysParams,
params: SearchIngestionKeysParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(

View File

@@ -453,18 +453,18 @@ export interface GatewaytypesGettableCreatedIngestionKeyDTO {
/**
* @type string
*/
id?: string;
id: string;
/**
* @type string
*/
value?: string;
value: string;
}
export interface GatewaytypesGettableCreatedIngestionKeyLimitDTO {
/**
* @type string
*/
id?: string;
id: string;
}
export interface GatewaytypesGettableIngestionKeysDTO {
@@ -616,7 +616,7 @@ export interface GatewaytypesPostableIngestionKeyDTO {
/**
* @type string
*/
name?: string;
name: string;
/**
* @type array
* @nullable true
@@ -638,7 +638,7 @@ export interface GatewaytypesPostableIngestionKeyLimitDTO {
}
export interface GatewaytypesUpdatableIngestionKeyLimitDTO {
config?: GatewaytypesLimitConfigDTO;
config: GatewaytypesLimitConfigDTO;
/**
* @type array
* @nullable true
@@ -2414,6 +2414,91 @@ export interface TypesUserDTO {
updatedAt?: Date;
}
export interface ZeustypesGettableHostDTO {
/**
* @type array
* @nullable true
*/
hosts: ZeustypesHostDTO[] | null;
/**
* @type string
*/
name: string;
/**
* @type string
*/
state: string;
/**
* @type string
*/
tier: string;
}
export interface ZeustypesHostDTO {
/**
* @type boolean
*/
is_default: boolean;
/**
* @type string
*/
name: string;
/**
* @type string
*/
url: string;
}
export interface ZeustypesPostableHostDTO {
/**
* @type string
*/
name: string;
}
export interface ZeustypesPostableProfileDTO {
/**
* @type string
*/
existing_observability_tool: string;
/**
* @type boolean
*/
has_existing_observability_tool: boolean;
/**
* @type integer
* @format int64
*/
logs_scale_per_day_in_gb: number;
/**
* @type integer
* @format int64
*/
number_of_hosts: number;
/**
* @type integer
* @format int64
*/
number_of_services: number;
/**
* @type array
* @nullable true
*/
reasons_for_interest_in_signoz: string[] | null;
/**
* @type string
*/
timeline_for_migrating_to_signoz: string;
/**
* @type boolean
*/
uses_otel: boolean;
/**
* @type string
*/
where_did_you_discover_signoz: string;
}
export type ChangePasswordPathParameters = {
id: string;
};
@@ -2954,7 +3039,7 @@ export type SearchIngestionKeysParams = {
* @type string
* @description undefined
*/
name?: string;
name: string;
/**
* @type integer
* @description undefined
@@ -3129,6 +3214,14 @@ export type RotateSession200 = {
status?: string;
};
export type GetHosts200 = {
data?: ZeustypesGettableHostDTO;
/**
* @type string
*/
status?: string;
};
export type QueryRangeV5200 = {
data?: Querybuildertypesv5QueryRangeResponseDTO;
/**

View File

@@ -0,0 +1,269 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useMutation, useQuery } from 'react-query';
import { GeneratedAPIInstance } from '../../../index';
import type {
GetHosts200,
RenderErrorResponseDTO,
ZeustypesPostableHostDTO,
ZeustypesPostableProfileDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint gets the host info from zeus.
* @summary Get host info from Zeus.
*/
export const getHosts = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetHosts200>({
url: `/api/v2/zeus/hosts`,
method: 'GET',
signal,
});
};
export const getGetHostsQueryKey = () => {
return ['getHosts'] as const;
};
export const getGetHostsQueryOptions = <
TData = Awaited<ReturnType<typeof getHosts>>,
TError = RenderErrorResponseDTO
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getHosts>>, TError, TData>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetHostsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getHosts>>> = ({
signal,
}) => getHosts(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getHosts>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetHostsQueryResult = NonNullable<
Awaited<ReturnType<typeof getHosts>>
>;
export type GetHostsQueryError = RenderErrorResponseDTO;
/**
* @summary Get host info from Zeus.
*/
export function useGetHosts<
TData = Awaited<ReturnType<typeof getHosts>>,
TError = RenderErrorResponseDTO
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getHosts>>, TError, TData>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetHostsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get host info from Zeus.
*/
export const invalidateGetHosts = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetHostsQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint saves the host of a deployment to zeus.
* @summary Put host in Zeus for a deployment.
*/
export const putHost = (zeustypesPostableHostDTO: ZeustypesPostableHostDTO) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/zeus/hosts`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: zeustypesPostableHostDTO,
});
};
export const getPutHostMutationOptions = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof putHost>>,
TError,
{ data: ZeustypesPostableHostDTO },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof putHost>>,
TError,
{ data: ZeustypesPostableHostDTO },
TContext
> => {
const mutationKey = ['putHost'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof putHost>>,
{ data: ZeustypesPostableHostDTO }
> = (props) => {
const { data } = props ?? {};
return putHost(data);
};
return { mutationFn, ...mutationOptions };
};
export type PutHostMutationResult = NonNullable<
Awaited<ReturnType<typeof putHost>>
>;
export type PutHostMutationBody = ZeustypesPostableHostDTO;
export type PutHostMutationError = RenderErrorResponseDTO;
/**
* @summary Put host in Zeus for a deployment.
*/
export const usePutHost = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof putHost>>,
TError,
{ data: ZeustypesPostableHostDTO },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof putHost>>,
TError,
{ data: ZeustypesPostableHostDTO },
TContext
> => {
const mutationOptions = getPutHostMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint saves the profile of a deployment to zeus.
* @summary Put profile in Zeus for a deployment.
*/
export const putProfile = (
zeustypesPostableProfileDTO: ZeustypesPostableProfileDTO,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/zeus/profiles`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: zeustypesPostableProfileDTO,
});
};
export const getPutProfileMutationOptions = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof putProfile>>,
TError,
{ data: ZeustypesPostableProfileDTO },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof putProfile>>,
TError,
{ data: ZeustypesPostableProfileDTO },
TContext
> => {
const mutationKey = ['putProfile'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof putProfile>>,
{ data: ZeustypesPostableProfileDTO }
> = (props) => {
const { data } = props ?? {};
return putProfile(data);
};
return { mutationFn, ...mutationOptions };
};
export type PutProfileMutationResult = NonNullable<
Awaited<ReturnType<typeof putProfile>>
>;
export type PutProfileMutationBody = ZeustypesPostableProfileDTO;
export type PutProfileMutationError = RenderErrorResponseDTO;
/**
* @summary Put profile in Zeus for a deployment.
*/
export const usePutProfile = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof putProfile>>,
TError,
{ data: ZeustypesPostableProfileDTO },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof putProfile>>,
TError,
{ data: ZeustypesPostableProfileDTO },
TContext
> => {
const mutationOptions = getPutProfileMutationOptions(options);
return useMutation(mutationOptions);
};

View File

@@ -34,6 +34,9 @@ function DashboardVariableSelection(): JSX.Element | null {
const sortedVariablesArray = useDashboardVariablesSelector(
(state) => state.sortedVariablesArray,
);
const dynamicVariableOrder = useDashboardVariablesSelector(
(state) => state.dynamicVariableOrder,
);
const dependencyData = useDashboardVariablesSelector(
(state) => state.dependencyData,
);
@@ -52,10 +55,11 @@ function DashboardVariableSelection(): JSX.Element | null {
}, [getUrlVariables, updateUrlVariable, dashboardVariables]);
// Memoize the order key to avoid unnecessary triggers
const dependencyOrderKey = useMemo(
() => dependencyData?.order?.join(',') ?? '',
[dependencyData?.order],
);
const variableOrderKey = useMemo(() => {
const queryVariableOrderKey = dependencyData?.order?.join(',') ?? '';
const dynamicVariableOrderKey = dynamicVariableOrder?.join(',') ?? '';
return `${queryVariableOrderKey}|${dynamicVariableOrderKey}`;
}, [dependencyData?.order, dynamicVariableOrder]);
// Initialize fetch store then start a new fetch cycle.
// Runs on dependency order changes, and time range changes.
@@ -66,7 +70,7 @@ function DashboardVariableSelection(): JSX.Element | null {
initializeVariableFetchStore(allVariableNames);
enqueueFetchOfAllVariables();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dependencyOrderKey, minTime, maxTime]);
}, [variableOrderKey, minTime, maxTime]);
// Performance optimization: For dynamic variables with allSelected=true, we don't store
// individual values in localStorage since we can always derive them from available options.

View File

@@ -0,0 +1,203 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { act, render } from '@testing-library/react';
import {
dashboardVariablesStore,
setDashboardVariablesStore,
updateDashboardVariablesStore,
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import {
IDashboardVariables,
IDashboardVariablesStoreState,
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import {
enqueueFetchOfAllVariables,
initializeVariableFetchStore,
} from 'providers/Dashboard/store/variableFetchStore';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import DashboardVariableSelection from '../DashboardVariableSelection';
// Mock providers/Dashboard/Dashboard
const mockSetSelectedDashboard = jest.fn();
const mockUpdateLocalStorageDashboardVariables = jest.fn();
jest.mock('providers/Dashboard/Dashboard', () => ({
useDashboard: (): Record<string, unknown> => ({
setSelectedDashboard: mockSetSelectedDashboard,
updateLocalStorageDashboardVariables: mockUpdateLocalStorageDashboardVariables,
}),
}));
// Mock hooks/dashboard/useVariablesFromUrl
const mockUpdateUrlVariable = jest.fn();
const mockGetUrlVariables = jest.fn().mockReturnValue({});
jest.mock('hooks/dashboard/useVariablesFromUrl', () => ({
__esModule: true,
default: (): Record<string, unknown> => ({
updateUrlVariable: mockUpdateUrlVariable,
getUrlVariables: mockGetUrlVariables,
}),
}));
// Mock variableFetchStore functions
jest.mock('providers/Dashboard/store/variableFetchStore', () => ({
initializeVariableFetchStore: jest.fn(),
enqueueFetchOfAllVariables: jest.fn(),
enqueueDescendantsOfVariable: jest.fn(),
}));
// Mock initializeDefaultVariables
jest.mock('providers/Dashboard/initializeDefaultVariables', () => ({
initializeDefaultVariables: jest.fn(),
}));
// Mock react-redux useSelector for globalTime
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }),
}));
// Mock VariableItem to avoid rendering complexity
jest.mock('../VariableItem', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="variable-item" />,
}));
function createVariable(
overrides: Partial<IDashboardVariable> = {},
): IDashboardVariable {
return {
id: 'test-id',
name: 'test-var',
description: '',
type: 'QUERY',
sort: 'DISABLED',
showALLOption: false,
multiSelect: false,
order: 0,
...overrides,
};
}
function resetStore(): void {
dashboardVariablesStore.set(() => ({
dashboardId: '',
variables: {},
sortedVariablesArray: [],
dependencyData: null,
variableTypes: {},
dynamicVariableOrder: [],
}));
}
describe('DashboardVariableSelection', () => {
beforeEach(() => {
resetStore();
jest.clearAllMocks();
});
it('should call initializeVariableFetchStore and enqueueFetchOfAllVariables on mount', () => {
const variables: IDashboardVariables = {
env: createVariable({ name: 'env', type: 'QUERY', order: 0 }),
};
setDashboardVariablesStore({ dashboardId: 'dash-1', variables });
render(<DashboardVariableSelection />);
expect(initializeVariableFetchStore).toHaveBeenCalledWith(['env']);
expect(enqueueFetchOfAllVariables).toHaveBeenCalled();
});
it('should re-trigger fetch cycle when dynamicVariableOrder changes', () => {
const variables: IDashboardVariables = {
env: createVariable({ name: 'env', type: 'QUERY', order: 0 }),
};
setDashboardVariablesStore({ dashboardId: 'dash-1', variables });
render(<DashboardVariableSelection />);
// Clear mocks after initial render
(initializeVariableFetchStore as jest.Mock).mockClear();
(enqueueFetchOfAllVariables as jest.Mock).mockClear();
// Add a DYNAMIC variable which changes dynamicVariableOrder
act(() => {
updateDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({ name: 'env', type: 'QUERY', order: 0 }),
dyn1: createVariable({ name: 'dyn1', type: 'DYNAMIC', order: 1 }),
},
});
});
expect(initializeVariableFetchStore).toHaveBeenCalledWith(
expect.arrayContaining(['env', 'dyn1']),
);
expect(enqueueFetchOfAllVariables).toHaveBeenCalled();
});
it('should re-trigger fetch cycle when a dynamic variable is removed', () => {
const variables: IDashboardVariables = {
env: createVariable({ name: 'env', type: 'QUERY', order: 0 }),
dyn1: createVariable({ name: 'dyn1', type: 'DYNAMIC', order: 1 }),
dyn2: createVariable({ name: 'dyn2', type: 'DYNAMIC', order: 2 }),
};
setDashboardVariablesStore({ dashboardId: 'dash-1', variables });
render(<DashboardVariableSelection />);
(initializeVariableFetchStore as jest.Mock).mockClear();
(enqueueFetchOfAllVariables as jest.Mock).mockClear();
// Remove dyn2, changing dynamicVariableOrder from ['dyn1','dyn2'] to ['dyn1']
act(() => {
updateDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({ name: 'env', type: 'QUERY', order: 0 }),
dyn1: createVariable({ name: 'dyn1', type: 'DYNAMIC', order: 1 }),
},
});
});
expect(initializeVariableFetchStore).toHaveBeenCalledWith(['env', 'dyn1']);
expect(enqueueFetchOfAllVariables).toHaveBeenCalled();
});
it('should NOT re-trigger fetch cycle when dynamicVariableOrder stays the same', () => {
const variables: IDashboardVariables = {
env: createVariable({ name: 'env', type: 'QUERY', order: 0 }),
dyn1: createVariable({ name: 'dyn1', type: 'DYNAMIC', order: 1 }),
};
setDashboardVariablesStore({ dashboardId: 'dash-1', variables });
render(<DashboardVariableSelection />);
(initializeVariableFetchStore as jest.Mock).mockClear();
(enqueueFetchOfAllVariables as jest.Mock).mockClear();
// Update a non-dynamic variable's selectedValue — dynamicVariableOrder unchanged
act(() => {
const snapshot = dashboardVariablesStore.getSnapshot();
dashboardVariablesStore.set(
(): IDashboardVariablesStoreState => ({
...snapshot,
variables: {
...snapshot.variables,
env: {
...snapshot.variables.env,
selectedValue: 'production',
},
},
}),
);
});
expect(initializeVariableFetchStore).not.toHaveBeenCalled();
expect(enqueueFetchOfAllVariables).not.toHaveBeenCalled();
});
});

View File

@@ -22,6 +22,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/zeus"
"github.com/gorilla/mux"
)
@@ -44,6 +45,7 @@ type provider struct {
gatewayHandler gateway.Handler
fieldsHandler fields.Handler
authzHandler authz.Handler
zeusHandler zeus.Handler
}
func NewFactory(
@@ -63,6 +65,7 @@ func NewFactory(
gatewayHandler gateway.Handler,
fieldsHandler fields.Handler,
authzHandler authz.Handler,
zeusHandler zeus.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(
@@ -85,6 +88,7 @@ func NewFactory(
gatewayHandler,
fieldsHandler,
authzHandler,
zeusHandler,
)
})
}
@@ -109,6 +113,7 @@ func newProvider(
gatewayHandler gateway.Handler,
fieldsHandler fields.Handler,
authzHandler authz.Handler,
zeusHandler zeus.Handler,
) (apiserver.APIServer, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
router := mux.NewRouter().UseEncodedPath()
@@ -131,6 +136,7 @@ func newProvider(
gatewayHandler: gatewayHandler,
fieldsHandler: fieldsHandler,
authzHandler: authzHandler,
zeusHandler: zeusHandler,
}
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
@@ -199,6 +205,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addZeusRoutes(router); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,65 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
"github.com/gorilla/mux"
)
func (provider *provider) addZeusRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/zeus/profiles", handler.New(provider.authZ.AdminAccess(provider.zeusHandler.PutProfile), handler.OpenAPIDef{
ID: "PutProfile",
Tags: []string{"zeus"},
Summary: "Put profile in Zeus for a deployment.",
Description: "This endpoint saves the profile of a deployment to zeus.",
Request: new(zeustypes.PostableProfile),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authZ.AdminAccess(provider.zeusHandler.GetHosts), handler.OpenAPIDef{
ID: "GetHosts",
Tags: []string{"zeus"},
Summary: "Get host info from Zeus.",
Description: "This endpoint gets the host info from zeus.",
Request: nil,
RequestContentType: "",
Response: new(zeustypes.GettableHost),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authZ.AdminAccess(provider.zeusHandler.PutHost), handler.OpenAPIDef{
ID: "PutHost",
Tags: []string{"zeus"},
Summary: "Put host in Zeus for a deployment.",
Description: "This endpoint saves the host of a deployment to zeus.",
Request: new(zeustypes.PostableHost),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -120,8 +120,6 @@ func FilterResponse(results []*qbtypes.QueryRangeResponse) []*qbtypes.QueryRange
}
}
resultData.Rows = filteredRows
case *qbtypes.ScalarData:
resultData.Data = filterScalarDataIPs(resultData.Columns, resultData.Data)
}
filteredData = append(filteredData, result)
@@ -147,39 +145,6 @@ func shouldIncludeSeries(series *qbtypes.TimeSeries) bool {
return true
}
func filterScalarDataIPs(columns []*qbtypes.ColumnDescriptor, data [][]any) [][]any {
// Find column indices for server address fields
serverColIndices := make([]int, 0)
for i, col := range columns {
if col.Name == derivedKeyHTTPHost {
serverColIndices = append(serverColIndices, i)
}
}
if len(serverColIndices) == 0 {
return data
}
filtered := make([][]any, 0, len(data))
for _, row := range data {
includeRow := true
for _, colIdx := range serverColIndices {
if colIdx < len(row) {
if strVal, ok := row[colIdx].(string); ok {
if net.ParseIP(strVal) != nil {
includeRow = false
break
}
}
}
}
if includeRow {
filtered = append(filtered, row)
}
}
return filtered
}
func shouldIncludeRow(row *qbtypes.RawRow) bool {
if row.Data != nil {
if domainVal, ok := row.Data[derivedKeyHTTPHost]; ok {

View File

@@ -117,59 +117,6 @@ func TestFilterResponse(t *testing.T) {
},
},
},
{
name: "should filter out IP addresses from scalar data",
input: []*qbtypes.QueryRangeResponse{
{
Data: qbtypes.QueryData{
Results: []any{
&qbtypes.ScalarData{
QueryName: "endpoints",
Columns: []*qbtypes.ColumnDescriptor{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: derivedKeyHTTPHost},
Type: qbtypes.ColumnTypeGroup,
},
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "endpoints"},
Type: qbtypes.ColumnTypeAggregation,
},
},
Data: [][]any{
{"192.168.1.1", 10},
{"example.com", 20},
{"10.0.0.1", 5},
},
},
},
},
},
},
expected: []*qbtypes.QueryRangeResponse{
{
Data: qbtypes.QueryData{
Results: []any{
&qbtypes.ScalarData{
QueryName: "endpoints",
Columns: []*qbtypes.ColumnDescriptor{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: derivedKeyHTTPHost},
Type: qbtypes.ColumnTypeGroup,
},
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "endpoints"},
Type: qbtypes.ColumnTypeAggregation,
},
},
Data: [][]any{
{"example.com", 20},
},
},
},
},
},
},
},
}
for _, tt := range tests {

View File

@@ -31,6 +31,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/zeus"
)
type Handlers struct {
@@ -48,6 +49,7 @@ type Handlers struct {
GatewayHandler gateway.Handler
Fields fields.Handler
AuthzHandler authz.Handler
ZeusHandler zeus.Handler
}
func NewHandlers(
@@ -60,6 +62,7 @@ func NewHandlers(
gatewayService gateway.Gateway,
telemetryMetadataStore telemetrytypes.MetadataStore,
authz authz.AuthZ,
zeusService zeus.Zeus,
) Handlers {
return Handlers{
SavedView: implsavedview.NewHandler(modules.SavedView),
@@ -76,5 +79,6 @@ func NewHandlers(
GatewayHandler: gateway.NewHandler(gatewayService),
Fields: implfields.NewHandler(providerSettings, telemetryMetadataStore),
AuthzHandler: signozauthzapi.NewHandler(authz),
ZeusHandler: zeus.NewHandler(zeusService, licensing),
}
}

View File

@@ -42,7 +42,7 @@ func TestNewHandlers(t *testing.T) {
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule)
handlers := NewHandlers(modules, providerSettings, nil, nil, nil, nil, nil, nil, nil)
handlers := NewHandlers(modules, providerSettings, nil, nil, nil, nil, nil, nil, nil, nil)
reflectVal := reflect.ValueOf(handlers)
for i := 0; i < reflectVal.NumField(); i++ {
f := reflectVal.Field(i)

View File

@@ -28,6 +28,7 @@ import (
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/gorilla/mux"
"github.com/SigNoz/signoz/pkg/zeus"
"github.com/swaggest/jsonschema-go"
"github.com/swaggest/openapi-go"
"github.com/swaggest/openapi-go/openapi3"
@@ -58,6 +59,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ gateway.Handler }{},
struct{ fields.Handler }{},
struct{ authz.Handler }{},
struct{ zeus.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
if err != nil {
return nil, err

View File

@@ -252,6 +252,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.GatewayHandler,
handlers.Fields,
handlers.AuthzHandler,
handlers.ZeusHandler,
),
)
}

View File

@@ -392,7 +392,7 @@ func New(
userService := impluser.NewService(providerSettings, impluser.NewStore(sqlstore, providerSettings), modules.User, orgGetter, authz, config.User.Root)
// Initialize all handlers for the modules
handlers := NewHandlers(modules, providerSettings, querier, licensing, global, flagger, gateway, telemetryMetadataStore, authz)
handlers := NewHandlers(modules, providerSettings, querier, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus)
// Initialize the API server
apiserver, err := factory.NewProviderFromNamedMap(

View File

@@ -40,7 +40,6 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
return nil, err
}
hooks := make([]telemetrystore.TelemetryStoreHook, len(hookFactories))
for i, hookFactory := range hookFactories {
hook, err := hookFactory.New(ctx, providerSettings, config)
@@ -83,11 +82,18 @@ func (p *provider) Query(ctx context.Context, query string, args ...interface{})
ctx = telemetrystore.WrapBeforeQuery(p.hooks, ctx, event)
rows, err := p.clickHouseConn.Query(ctx, query, args...)
if err != nil {
event.Err = err
telemetrystore.WrapAfterQuery(p.hooks, ctx, event)
return nil, err
}
event.Err = err
telemetrystore.WrapAfterQuery(p.hooks, ctx, event)
return rows, err
return &rowsWithHooks{
Rows: rows,
ctx: ctx,
event: event,
onClose: func() { telemetrystore.WrapAfterQuery(p.hooks, ctx, event) },
}, nil
}
func (p *provider) QueryRow(ctx context.Context, query string, args ...interface{}) driver.Row {

View File

@@ -0,0 +1,37 @@
package clickhousetelemetrystore
import (
"context"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
"github.com/SigNoz/signoz/pkg/telemetrystore"
)
// rowsWithHooks wraps driver.Rows and defers AfterQuery hooks until Close(),
// so the instrumentation span covers the full query lifecycle including row consumption.
type rowsWithHooks struct {
driver.Rows
ctx context.Context
event *telemetrystore.QueryEvent
onClose func()
closed bool
}
func (r *rowsWithHooks) Close() error {
// delegate to the original rows.Close() if already closed
if r.closed {
return r.Rows.Close()
}
// mark as closed and run the onClose hook
r.closed = true
if err := r.Rows.Err(); err != nil {
r.event.Err = err
}
closeErr := r.Rows.Close()
if closeErr != nil {
r.event.Err = closeErr
}
r.onClose()
return closeErr
}

View File

@@ -60,7 +60,7 @@ type IngestionKeysParams struct {
}
type SearchIngestionKeysParams struct {
Name string `query:"name"`
Name string `query:"name" required:"true"`
Page int `query:"page"`
PerPage int `query:"per_page"`
}
@@ -71,14 +71,14 @@ type GettableIngestionKeys struct {
}
type PostableIngestionKey struct {
Name string `json:"name"`
Name string `json:"name" required:"true"`
Tags []string `json:"tags"`
ExpiresAt time.Time `json:"expires_at"`
}
type GettableCreatedIngestionKey struct {
ID string `json:"id"`
Value string `json:"value"`
ID string `json:"id" required:"true"`
Value string `json:"value" required:"true"`
}
type PostableIngestionKeyLimit struct {
@@ -88,10 +88,10 @@ type PostableIngestionKeyLimit struct {
}
type GettableCreatedIngestionKeyLimit struct {
ID string `json:"id"`
ID string `json:"id" required:"true"`
}
type UpdatableIngestionKeyLimit struct {
Config LimitConfig `json:"config"`
Config LimitConfig `json:"config" required:"true"`
Tags []string `json:"tags"`
}

View File

@@ -0,0 +1,58 @@
package zeustypes
import (
"net/url"
"github.com/tidwall/gjson"
)
type PostableHost struct {
Name string `json:"name" required:"true"`
}
type PostableProfile struct {
UsesOtel bool `json:"uses_otel" required:"true"`
HasExistingObservabilityTool bool `json:"has_existing_observability_tool" required:"true"`
ExistingObservabilityTool string `json:"existing_observability_tool" required:"true"`
ReasonsForInterestInSigNoz []string `json:"reasons_for_interest_in_signoz" required:"true"`
LogsScalePerDayInGB int64 `json:"logs_scale_per_day_in_gb" required:"true"`
NumberOfServices int64 `json:"number_of_services" required:"true"`
NumberOfHosts int64 `json:"number_of_hosts" required:"true"`
WhereDidYouDiscoverSigNoz string `json:"where_did_you_discover_signoz" required:"true"`
TimelineForMigratingToSigNoz string `json:"timeline_for_migrating_to_signoz" required:"true"`
}
type GettableHost struct {
Name string `json:"name" required:"true"`
State string `json:"state" required:"true"`
Tier string `json:"tier" required:"true"`
Hosts []Host `json:"hosts" required:"true"`
}
type Host struct {
Name string `json:"name" required:"true"`
IsDefault bool `json:"is_default" required:"true"`
URL string `json:"url" required:"true"`
}
func NewGettableHost(data []byte) *GettableHost {
parsed := gjson.ParseBytes(data)
dns := parsed.Get("cluster.region.dns").String()
hostResults := parsed.Get("hosts").Array()
hosts := make([]Host, len(hostResults))
for i, h := range hostResults {
name := h.Get("name").String()
hosts[i].Name = name
hosts[i].IsDefault = h.Get("is_default").Bool()
hosts[i].URL = (&url.URL{Scheme: "https", Host: name + "." + dns}).String()
}
return &GettableHost{
Name: parsed.Get("name").String(),
State: parsed.Get("state").String(),
Tier: parsed.Get("tier").String(),
Hosts: hosts,
}
}

114
pkg/zeus/handler.go Normal file
View File

@@ -0,0 +1,114 @@
package zeus
import (
"net/http"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type handler struct {
zeus Zeus
licensing licensing.Licensing
}
func NewHandler(zeus Zeus, licensing licensing.Licensing) Handler {
return &handler{
zeus: zeus,
licensing: licensing,
}
}
func (h *handler) PutProfile(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
license, err := h.licensing.GetActive(ctx, valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, err)
return
}
req := new(zeustypes.PostableProfile)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
if err := h.zeus.PutProfile(ctx, license.Key, req); err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (h *handler) GetHosts(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
license, err := h.licensing.GetActive(ctx, valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, err)
return
}
deploymentBytes, err := h.zeus.GetDeployment(ctx, license.Key)
if err != nil {
render.Error(rw, err)
return
}
response := zeustypes.NewGettableHost(deploymentBytes)
render.Success(rw, http.StatusOK, response)
}
func (h *handler) PutHost(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
license, err := h.licensing.GetActive(ctx, valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, err)
return
}
req := new(zeustypes.PostableHost)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
if req.Name == "" {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "name is required"))
return
}
if err := h.zeus.PutHost(ctx, license.Key, req); err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
"github.com/SigNoz/signoz/pkg/zeus"
)
@@ -40,10 +41,10 @@ func (provider *provider) PutMeters(_ context.Context, _ string, _ []byte) error
return errors.New(errors.TypeUnsupported, zeus.ErrCodeUnsupported, "putting meters is not supported")
}
func (provider *provider) PutProfile(_ context.Context, _ string, _ []byte) error {
func (provider *provider) PutProfile(_ context.Context, _ string, _ *zeustypes.PostableProfile) error {
return errors.New(errors.TypeUnsupported, zeus.ErrCodeUnsupported, "putting profile is not supported")
}
func (provider *provider) PutHost(_ context.Context, _ string, _ []byte) error {
func (provider *provider) PutHost(_ context.Context, _ string, _ *zeustypes.PostableHost) error {
return errors.New(errors.TypeUnsupported, zeus.ErrCodeUnsupported, "putting host is not supported")
}

View File

@@ -2,8 +2,10 @@ package zeus
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
)
var (
@@ -28,8 +30,19 @@ type Zeus interface {
PutMeters(context.Context, string, []byte) error
// Put profile for the given license key.
PutProfile(context.Context, string, []byte) error
PutProfile(context.Context, string, *zeustypes.PostableProfile) error
// Put host for the given license key.
PutHost(context.Context, string, []byte) error
PutHost(context.Context, string, *zeustypes.PostableHost) error
}
type Handler interface {
// API level handler for PutProfile
PutProfile(http.ResponseWriter, *http.Request)
// API level handler for getting hosts a slim wrapper around GetDeployment
GetHosts(http.ResponseWriter, *http.Request)
// API level handler for PutHost
PutHost(http.ResponseWriter, *http.Request)
}