Compare commits

..

1 Commits

Author SHA1 Message Date
nityanandagohain
b43061fff1 feat: system dashboards 2026-04-12 21:57:53 +05:30
15 changed files with 514 additions and 32 deletions

View File

@@ -2698,6 +2698,37 @@ components:
- name
- expiresAt
type: object
SystemdashboardtypesData:
additionalProperties: {}
type: object
SystemdashboardtypesGettableSystemDashboard:
properties:
created_at:
format: date-time
type: string
data:
$ref: '#/components/schemas/SystemdashboardtypesData'
id:
type: string
source:
type: string
updated_at:
format: date-time
type: string
required:
- id
- source
- data
- created_at
- updated_at
type: object
SystemdashboardtypesUpdatableSystemDashboard:
properties:
data:
$ref: '#/components/schemas/SystemdashboardtypesData'
required:
- data
type: object
TelemetrytypesFieldContext:
enum:
- metric
@@ -6434,6 +6465,133 @@ paths:
summary: Updates my service account
tags:
- serviceaccount
/api/v1/system/{source}/dashboard:
get:
deprecated: false
description: Returns the system dashboard for the given source. Seeds default
panels on the first call if no dashboard exists yet.
operationId: GetSystemDashboard
parameters:
- in: path
name: source
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/SystemdashboardtypesGettableSystemDashboard'
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: Get a system dashboard
tags:
- system-dashboard
put:
deprecated: false
description: Replaces the panels, layout, and variables of a system dashboard.
operationId: UpdateSystemDashboard
parameters:
- in: path
name: source
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SystemdashboardtypesUpdatableSystemDashboard'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/SystemdashboardtypesGettableSystemDashboard'
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 a system dashboard
tags:
- system-dashboard
/api/v1/user:
get:
deprecated: false

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"github.com/SigNoz/signoz/ee/query-service/constants"
"github.com/SigNoz/signoz/ee/query-service/model"
)
@@ -44,12 +45,15 @@ type details struct {
BillTotal float64 `json:"billTotal"`
}
type billingData struct {
BillingPeriodStart int64 `json:"billingPeriodStart"`
BillingPeriodEnd int64 `json:"billingPeriodEnd"`
Details details `json:"details"`
Discount float64 `json:"discount"`
SubscriptionStatus string `json:"subscriptionStatus"`
type billingDetails struct {
Status string `json:"status"`
Data struct {
BillingPeriodStart int64 `json:"billingPeriodStart"`
BillingPeriodEnd int64 `json:"billingPeriodEnd"`
Details details `json:"details"`
Discount float64 `json:"discount"`
SubscriptionStatus string `json:"subscriptionStatus"`
} `json:"data"`
}
func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
@@ -60,17 +64,28 @@ func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
return
}
data, err := ah.Signoz.Zeus.GetMeters(r.Context(), licenseKey)
billingURL := fmt.Sprintf("%s/usage?licenseKey=%s", constants.LicenseSignozIo, licenseKey)
hClient := &http.Client{}
req, err := http.NewRequest("GET", billingURL, nil)
if err != nil {
RespondError(w, model.InternalError(err), nil)
return
}
req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey)
billingResp, err := hClient.Do(req)
if err != nil {
RespondError(w, model.InternalError(err), nil)
return
}
var billing billingData
if err := json.Unmarshal(data, &billing); err != nil {
// decode response body
var billingResponse billingDetails
if err := json.NewDecoder(billingResp.Body).Decode(&billingResponse); err != nil {
RespondError(w, model.InternalError(err), nil)
return
}
ah.Respond(w, billing)
// TODO(srikanthccv):Fetch the current day usage and add it to the response
ah.Respond(w, billingResponse.Data)
}

View File

@@ -109,21 +109,6 @@ func (provider *Provider) GetDeployment(ctx context.Context, key string) ([]byte
return []byte(gjson.GetBytes(response, "data").String()), nil
}
func (provider *Provider) GetMeters(ctx context.Context, key string) ([]byte, error) {
response, err := provider.do(
ctx,
provider.config.URL.JoinPath("/v1/meters"),
http.MethodGet,
key,
nil,
)
if err != nil {
return nil, err
}
return []byte(gjson.GetBytes(response, "data").String()), nil
}
func (provider *Provider) PutMeters(ctx context.Context, key string, data []byte) error {
_, err := provider.do(
ctx,

View File

@@ -22,6 +22,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
@@ -57,6 +58,7 @@ type provider struct {
factoryHandler factory.Handler
cloudIntegrationHandler cloudintegration.Handler
ruleStateHistoryHandler rulestatehistory.Handler
systemDashboardHandler systemdashboard.Handler
}
func NewFactory(
@@ -83,6 +85,7 @@ func NewFactory(
factoryHandler factory.Handler,
cloudIntegrationHandler cloudintegration.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
systemDashboardHandler systemdashboard.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(
@@ -112,6 +115,7 @@ func NewFactory(
factoryHandler,
cloudIntegrationHandler,
ruleStateHistoryHandler,
systemDashboardHandler,
)
})
}
@@ -143,6 +147,7 @@ func newProvider(
factoryHandler factory.Handler,
cloudIntegrationHandler cloudintegration.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
systemDashboardHandler systemdashboard.Handler,
) (apiserver.APIServer, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
router := mux.NewRouter().UseEncodedPath()
@@ -172,6 +177,7 @@ func newProvider(
factoryHandler: factoryHandler,
cloudIntegrationHandler: cloudIntegrationHandler,
ruleStateHistoryHandler: ruleStateHistoryHandler,
systemDashboardHandler: systemDashboardHandler,
}
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
@@ -272,6 +278,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addSystemDashboardRoutes(router); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,54 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/systemdashboardtypes"
"github.com/gorilla/mux"
)
func (provider *provider) addSystemDashboardRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/system/{source}/dashboard", handler.New(
provider.authZ.ViewAccess(provider.systemDashboardHandler.Get),
handler.OpenAPIDef{
ID: "GetSystemDashboard",
Tags: []string{"system-dashboard"},
Summary: "Get a system dashboard",
Description: "Returns the system dashboard for the given source. Seeds default panels on the first call if no dashboard exists yet.",
Request: nil,
RequestContentType: "",
Response: new(systemdashboardtypes.GettableSystemDashboard),
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/v1/system/{source}/dashboard", handler.New(
provider.authZ.EditAccess(provider.systemDashboardHandler.Update),
handler.OpenAPIDef{
ID: "UpdateSystemDashboard",
Tags: []string{"system-dashboard"},
Summary: "Update a system dashboard",
Description: "Replaces the panels, layout, and variables of a system dashboard.",
Request: new(systemdashboardtypes.UpdatableSystemDashboard),
RequestContentType: "application/json",
Response: new(systemdashboardtypes.GettableSystemDashboard),
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
}
return nil
}

View File

@@ -0,0 +1,103 @@
package implsystemdashboard
import (
"context"
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/systemdashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
type handler struct {
module systemdashboard.Module
providerSettings factory.ProviderSettings
}
func NewHandler(module systemdashboard.Module, providerSettings factory.ProviderSettings) systemdashboard.Handler {
return &handler{module: module, providerSettings: providerSettings}
}
func (h *handler) Get(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
source, err := sourceFromPath(r)
if err != nil {
render.Error(rw, err)
return
}
dashboard, err := h.module.Get(ctx, orgID, source)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, systemdashboardtypes.NewGettableSystemDashboard(dashboard))
}
func (h *handler) Update(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
source, err := sourceFromPath(r)
if err != nil {
render.Error(rw, err)
return
}
req := new(systemdashboardtypes.UpdatableSystemDashboard)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
dashboard, err := h.module.Update(ctx, orgID, source, claims.Email, req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, systemdashboardtypes.NewGettableSystemDashboard(dashboard))
}
// sourceFromPath extracts the {source} path variable.
func sourceFromPath(r *http.Request) (string, error) {
source := mux.Vars(r)["source"]
if source == "" {
return "", errors.Newf(errors.TypeInvalidInput, systemdashboardtypes.ErrCodeSystemDashboardInvalidInput, "source is missing from the path")
}
return source, nil
}

View File

@@ -0,0 +1,24 @@
package systemdashboard
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/types/systemdashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
// System dashboards are pre-seeded, org-scoped dashboards identified by a source
type Module interface {
// Get returns the system dashboard for the given org and source,
// seeding default panels if the dashboard does not yet exist.
Get(ctx context.Context, orgID valuer.UUID, source string) (*systemdashboardtypes.SystemDashboard, error)
// Update replaces the data blob of an existing system dashboard.
Update(ctx context.Context, orgID valuer.UUID, source string, updatedBy string, req *systemdashboardtypes.UpdatableSystemDashboard) (*systemdashboardtypes.SystemDashboard, error)
}
type Handler interface {
Get(rw http.ResponseWriter, r *http.Request)
Update(rw http.ResponseWriter, r *http.Request)
}

View File

@@ -26,6 +26,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/rawdataexport/implrawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory/implrulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/modules/savedview"
"github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
@@ -62,6 +63,7 @@ type Handlers struct {
RegistryHandler factory.Handler
CloudIntegrationHandler cloudintegration.Handler
RuleStateHistory rulestatehistory.Handler
SystemDashboardHandler systemdashboard.Handler
}
func NewHandlers(

View File

@@ -27,6 +27,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
@@ -69,6 +70,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ factory.Handler }{},
struct{ cloudintegration.Handler }{},
struct{ rulestatehistory.Handler }{},
struct{ systemdashboard.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
if err != nil {
return nil, err

View File

@@ -288,6 +288,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.RegistryHandler,
handlers.CloudIntegrationHandler,
handlers.RuleStateHistory,
handlers.SystemDashboardHandler,
),
)
}

View File

@@ -0,0 +1,113 @@
package systemdashboardtypes
import (
"database/sql/driver"
"encoding/json"
"fmt"
"time"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
// Data is the panel + layout + variables blob for a system dashboard.
// It uses the same format as dashboard.data so the frontend can render it
// with the existing dashboard component. Stored as a JSON text column.
type Data map[string]any
func (d Data) Value() (driver.Value, error) {
if d == nil {
return "{}", nil
}
b, err := json.Marshal(d)
if err != nil {
return nil, err
}
return string(b), nil
}
func (d *Data) Scan(src any) error {
var raw []byte
switch v := src.(type) {
case string:
raw = []byte(v)
case []byte:
raw = v
case nil:
*d = Data{}
return nil
default:
return fmt.Errorf("systemdashboardtypes: cannot scan %T into Data", src)
}
return json.Unmarshal(raw, d)
}
// StorableSystemDashboard is the bun/DB representation of a system dashboard.
// Each (org_id, source) pair is unique — the source identifies which pre-seeded
// page the dashboard belongs to (e.g. "ai-o11y-overview", "service-overview").
type StorableSystemDashboard struct {
bun.BaseModel `bun:"table:system_dashboard,alias:system_dashboard"`
types.Identifiable
types.TimeAuditable
OrgID valuer.UUID `bun:"org_id,type:text,notnull"`
Source string `bun:"source,type:text,notnull"`
Data Data `bun:"data,type:text,notnull"`
}
// SystemDashboard is the domain model for a system dashboard.
type SystemDashboard struct {
types.TimeAuditable
ID string
OrgID valuer.UUID
Source string
Data Data
}
// NewSystemDashboardFromStorable converts a StorableSystemDashboard to a SystemDashboard.
func NewSystemDashboardFromStorable(s *StorableSystemDashboard) *SystemDashboard {
data := make(Data, len(s.Data))
for k, v := range s.Data {
data[k] = v
}
return &SystemDashboard{
TimeAuditable: s.TimeAuditable,
ID: s.ID.StringValue(),
OrgID: s.OrgID,
Source: s.Source,
Data: data,
}
}
// GettableSystemDashboard is the HTTP response representation of a system dashboard.
type GettableSystemDashboard struct {
ID string `json:"id" required:"true"`
Source string `json:"source" required:"true"`
Data Data `json:"data" required:"true"`
CreatedAt time.Time `json:"created_at" required:"true"`
UpdatedAt time.Time `json:"updated_at" required:"true"`
}
// NewGettableSystemDashboard converts a domain SystemDashboard to a GettableSystemDashboard.
func NewGettableSystemDashboard(d *SystemDashboard) *GettableSystemDashboard {
data := make(Data, len(d.Data))
for k, v := range d.Data {
data[k] = v
}
return &GettableSystemDashboard{
ID: d.ID,
Source: d.Source,
Data: data,
CreatedAt: d.CreatedAt,
UpdatedAt: d.UpdatedAt,
}
}
// UpdatableSystemDashboard is the HTTP request body for updating a system dashboard.
// The entire data blob is replaced on PUT.
type UpdatableSystemDashboard struct {
Data Data `json:"data" required:"true"`
}

View File

@@ -0,0 +1,9 @@
package systemdashboardtypes
import "github.com/SigNoz/signoz/pkg/errors"
var (
ErrCodeSystemDashboardNotFound = errors.MustNewCode("system_dashboard_not_found")
ErrCodeSystemDashboardInvalidInput = errors.MustNewCode("system_dashboard_invalid_input")
ErrCodeSystemDashboardInvalidData = errors.MustNewCode("system_dashboard_invalid_data")
)

View File

@@ -0,0 +1,13 @@
package systemdashboardtypes
import (
"context"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Store interface {
Get(ctx context.Context, orgID valuer.UUID, source string) (*StorableSystemDashboard, error)
Create(ctx context.Context, dashboard *StorableSystemDashboard) error
Update(ctx context.Context, dashboard *StorableSystemDashboard) error
}

View File

@@ -37,10 +37,6 @@ func (provider *provider) GetDeployment(_ context.Context, _ string) ([]byte, er
return nil, errors.New(errors.TypeUnsupported, zeus.ErrCodeUnsupported, "getting the deployment is not supported")
}
func (provider *provider) GetMeters(_ context.Context, _ string) ([]byte, error) {
return nil, errors.New(errors.TypeUnsupported, zeus.ErrCodeUnsupported, "getting meters is not supported")
}
func (provider *provider) PutMeters(_ context.Context, _ string, _ []byte) error {
return errors.New(errors.TypeUnsupported, zeus.ErrCodeUnsupported, "putting meters is not supported")
}

View File

@@ -26,9 +26,6 @@ type Zeus interface {
// Returns the deployment for the given license key.
GetDeployment(context.Context, string) ([]byte, error)
// Returns the billing details for the given license key.
GetMeters(context.Context, string) ([]byte, error)
// Puts the meters for the given license key.
PutMeters(context.Context, string, []byte) error