api/api.go
Rama main · réplica de lectura del repositorio original.
package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"slices"
"sort"
"strconv"
"strings"
"time"
"github.com/coroot/coroot/api/forms"
"github.com/coroot/coroot/api/views"
"github.com/coroot/coroot/auditor"
"github.com/coroot/coroot/cache"
"github.com/coroot/coroot/ch"
"github.com/coroot/coroot/clickhouse"
pricing "github.com/coroot/coroot/cloud-pricing"
"github.com/coroot/coroot/collector"
"github.com/coroot/coroot/config"
"github.com/coroot/coroot/constructor"
"github.com/coroot/coroot/db"
"github.com/coroot/coroot/model"
"github.com/coroot/coroot/notifications"
"github.com/coroot/coroot/prom"
"github.com/coroot/coroot/rbac"
"github.com/coroot/coroot/stats"
"github.com/coroot/coroot/timeseries"
"github.com/coroot/coroot/utils"
"github.com/gorilla/mux"
"golang.org/x/exp/maps"
"gopkg.in/yaml.v3"
"k8s.io/klog"
)
const (
MaxIncidentWindow = timeseries.Day
MaxRCAWindow = 4 * timeseries.Hour
)
type LoadWorldF func(ctx context.Context, project *db.Project, from, to timeseries.Time) (*model.World, error)
type Api struct {
cfg *config.Config
cache *cache.Cache
db *db.DB
collector *collector.Collector
stats *stats.Collector
pricing *pricing.Manager
roles rbac.RoleManager
globalClickHouse *db.IntegrationClickhouse
globalPrometheus *db.IntegrationPrometheus
licenseMgr LicenseManager
authSecret string
authAnonymousRole rbac.RoleName
deploymentUuid string
instanceUuid string
loadWorld LoadWorldF
}
func NewApi(cfg *config.Config, cache *cache.Cache, db *db.DB, collector *collector.Collector, stats *stats.Collector, pricing *pricing.Manager, roles rbac.RoleManager, licenseMgr LicenseManager,
globalClickHouse *db.IntegrationClickhouse, globalPrometheus *db.IntegrationPrometheus,
deploymentUuid, instanceUuid string, loadWorld LoadWorldF) *Api {
return &Api{
cfg: cfg,
cache: cache,
db: db,
collector: collector,
stats: stats,
pricing: pricing,
roles: roles,
globalClickHouse: globalClickHouse,
globalPrometheus: globalPrometheus,
licenseMgr: licenseMgr,
deploymentUuid: deploymentUuid,
instanceUuid: instanceUuid,
loadWorld: loadWorld,
}
}
func (api *Api) User(w http.ResponseWriter, r *http.Request, u *db.User) {
if r.Method == http.MethodPost {
if u.Anonymous {
return
}
var form forms.ChangePasswordForm
if err := forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "", http.StatusBadRequest)
return
}
if err := api.db.ChangeUserPassword(u.Id, form.OldPassword, form.NewPassword); err != nil {
klog.Errorln(err)
switch {
case errors.Is(err, db.ErrNotFound):
http.Error(w, "User not found.", http.StatusNotFound)
case errors.Is(err, db.ErrInvalid):
http.Error(w, "Invalid old password.", http.StatusBadRequest)
case errors.Is(err, db.ErrConflict):
http.Error(w, "New password can't be the same as the old one.", http.StatusBadRequest)
default:
http.Error(w, "", http.StatusInternalServerError)
}
return
}
return
}
projects, err := api.db.GetProjectNames()
if err != nil {
klog.Errorln("failed to get projects:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
viewonly := !api.IsAllowed(u, rbac.Actions.Project("*").Settings().Edit())
utils.WriteJson(w, views.User(u, projects, viewonly))
}
func (api *Api) Users(w http.ResponseWriter, r *http.Request, u *db.User) {
if !api.IsAllowed(u, rbac.Actions.Users().Edit()) {
http.Error(w, "You are not allowed to edit users.", http.StatusForbidden)
return
}
roles, err := api.roles.GetRoles()
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if r.Method == http.MethodPost {
var form forms.UserForm
if err := forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "", http.StatusBadRequest)
return
}
if form.Email == db.AdminUserLogin {
return
}
switch form.Action {
case forms.UserActionCreate:
if !form.Role.Valid(roles) {
http.Error(w, "Unknown role: "+form.Name, http.StatusBadRequest)
return
}
var err error
if form.ServiceAccount {
_, err = api.db.AddServiceAccount(form.Email, form.Name, form.Role)
} else {
err = api.db.AddUser(form.Email, form.Password, form.Name, form.Role)
}
if err != nil {
klog.Errorln(err)
if errors.Is(err, db.ErrConflict) {
http.Error(w, "The user is already added.", http.StatusConflict)
return
}
http.Error(w, "", http.StatusInternalServerError)
return
}
case forms.UserActionUpdate:
if !form.Role.Valid(roles) {
http.Error(w, "Unknown role: "+form.Name, http.StatusBadRequest)
return
}
if err := api.db.UpdateUser(form.Id, form.Email, form.Password, form.Name, form.Role); err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
case forms.UserActionDelete:
if err := api.db.DeleteUser(form.Id); err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
}
return
}
users, err := api.db.GetUsers()
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
utils.WriteJson(w, views.Users(users, roles, api.isConfigServiceAccount))
}
func (api *Api) UserApiKeys(w http.ResponseWriter, r *http.Request, u *db.User) {
userId, err := strconv.Atoi(mux.Vars(r)["user"])
if err != nil {
http.Error(w, "", http.StatusBadRequest)
return
}
own := !u.Anonymous && u.Id == userId
if !own && !api.IsAllowed(u, rbac.Actions.Users().Edit()) {
http.Error(w, "You are not allowed to manage API keys of other users.", http.StatusForbidden)
return
}
if r.Method == http.MethodPost {
if target, err := api.db.GetUser(userId); err != nil || api.isConfigServiceAccount(target) {
http.Error(w, "API keys of config-defined service accounts are managed in the config file.", http.StatusForbidden)
return
}
var form forms.UserApiKeyForm
if err := forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "", http.StatusBadRequest)
return
}
switch form.Action {
case forms.UserActionCreate:
key := "crt_" + utils.RandomString(32)
if err := api.db.AddUserApiKey(userId, key, form.Description); err != nil {
if errors.Is(err, db.ErrConflict) {
http.Error(w, "An API key with this description already exists.", http.StatusConflict)
return
}
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
utils.WriteJson(w, map[string]string{"key": key})
case forms.UserActionDelete:
if err := api.db.DeleteUserApiKey(userId, form.Id); err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
}
return
}
keys, err := api.db.GetUserApiKeys(userId)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
utils.WriteJson(w, keys)
}
func (api *Api) Roles(w http.ResponseWriter, r *http.Request, u *db.User) {
if r.Method == http.MethodPost {
http.Error(w, "", http.StatusMethodNotAllowed)
return
}
qaSample := rbac.NewRole("QA",
rbac.NewPermission(rbac.ScopeProjectAll, rbac.ActionAll, rbac.Object{"project_id": "staging"}),
)
dbaSample := rbac.NewRole("DBA",
rbac.NewPermission(rbac.ScopeProjectInstrumentations, rbac.ActionEdit, nil),
rbac.NewPermission(rbac.ScopeApplication, rbac.ActionView, rbac.Object{"application_category": "databases"}),
rbac.NewPermission(rbac.ScopeNode, rbac.ActionView, rbac.Object{"node_name": "db*"}),
rbac.NewPermission(rbac.ScopeDashboard, rbac.ActionView, rbac.Object{"dashboard_name": "db*"}),
)
roles, err := api.roles.GetRoles()
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
utils.WriteJson(w, views.Roles(append(roles, qaSample, dbaSample)))
}
func (api *Api) SSO(w http.ResponseWriter, r *http.Request, u *db.User) {
roles, err := api.roles.GetRoles()
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
res := struct {
Roles []rbac.RoleName `json:"roles"`
DefaultRole rbac.RoleName `json:"default_role"`
}{
DefaultRole: rbac.RoleViewer,
}
for _, role := range roles {
res.Roles = append(res.Roles, role.Name)
}
utils.WriteJson(w, res)
}
func (api *Api) AI(w http.ResponseWriter, r *http.Request, u *db.User) {
res := struct {
Provider string `json:"provider"`
}{}
utils.WriteJson(w, res)
}
func (api *Api) Project(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
isAllowed := api.IsAllowed(u, rbac.Actions.Project(projectId).Settings().Edit())
switch r.Method {
case http.MethodGet:
type ProjectSettings struct {
Readonly bool `json:"readonly"`
Name string `json:"name"`
ApiKeys any `json:"api_keys"`
RefreshInterval timeseries.Duration `json:"refresh_interval"`
MemberProjects []string `json:"member_projects,omitempty"`
AvailableProjects []string `json:"available_projects,omitempty"`
}
projects, err := api.db.GetProjects()
if err != nil {
klog.Errorln("failed to get project names:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
res := ProjectSettings{}
for _, p := range projects {
if p.Id != db.ProjectId(projectId) && !p.Multicluster() {
res.AvailableProjects = append(res.AvailableProjects, p.Name)
}
}
if projectId != "" {
project, err := api.db.GetProject(db.ProjectId(projectId))
if err != nil {
if errors.Is(err, db.ErrNotFound) {
klog.Warningln("project not found:", projectId)
return
}
klog.Errorln("failed to get project:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
prometheusCfg := project.PrometheusConfig(api.globalPrometheus)
res.Readonly = project.Settings.Readonly
res.Name = project.Name
res.RefreshInterval = prometheusCfg.RefreshInterval
res.MemberProjects = project.Settings.MemberProjects
if isAllowed {
res.ApiKeys = project.Settings.ApiKeys
} else {
res.ApiKeys = "permission denied"
}
}
utils.WriteJson(w, res)
case http.MethodPost:
if !isAllowed {
http.Error(w, "You are not allowed to configure the project.", http.StatusForbidden)
return
}
var form forms.ProjectForm
if err := forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "", http.StatusBadRequest)
return
}
isNew := projectId == ""
var project *db.Project
var err error
if isNew {
project = &db.Project{
Name: form.Name,
}
project.Settings.MemberProjects = form.MemberProjects
project.Settings.Readonly = false
} else {
project, err = api.db.GetProject(db.ProjectId(projectId))
if err != nil {
if errors.Is(err, db.ErrNotFound) {
http.Error(w, "Project not found.", http.StatusNotFound)
return
}
klog.Errorln("failed to get project:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if project.Settings.Readonly {
http.Error(w, "This project is defined through the config and cannot be modified via the UI.", http.StatusForbidden)
return
}
project.Name = form.Name
project.Settings.MemberProjects = form.MemberProjects
}
if project.Multicluster() {
project.Settings.ApiKeys = nil
}
project.Id = db.ProjectId(projectId)
err = api.db.SaveProject(project)
if err != nil {
if errors.Is(err, db.ErrConflict) {
http.Error(w, "This project name is already being used.", http.StatusConflict)
return
}
klog.Errorln("failed to save project:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if err = api.db.SaveProjectSettings(project); err != nil {
klog.Errorln("failed to save project settings:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if isNew && !project.Multicluster() && api.globalClickHouse != nil {
err = api.collector.MigrateClickhouseDatabase(r.Context(), project)
if err != nil {
klog.Errorln("failed to migrate clickhouse database:", err)
http.Error(w, "Failed to create or update clickhouse database", http.StatusInternalServerError)
return
}
}
http.Error(w, string(project.Id), http.StatusOK)
case http.MethodDelete:
if !isAllowed {
http.Error(w, "You are not allowed to delete the project.", http.StatusForbidden)
return
}
if err := api.db.DeleteProject(db.ProjectId(projectId)); err != nil {
klog.Errorln("failed to delete project:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
http.Error(w, "", http.StatusOK)
default:
http.Error(w, "", http.StatusMethodNotAllowed)
}
}
func (api *Api) Status(w http.ResponseWriter, r *http.Request, u *db.User) {
projectId := db.ProjectId(mux.Vars(r)["project"])
project, err := api.db.GetProject(projectId)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
klog.Warningln("project not found:", projectId)
utils.WriteJson(w, Status{})
return
}
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
now := timeseries.Now()
world, cacheStatus, err := api.LoadWorld(r.Context(), project, now.Add(-timeseries.Hour), now)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, renderStatus(project, cacheStatus, world, api.globalPrometheus)))
}
func (api *Api) Overview(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
view := vars["view"]
switch view {
case "traces":
if !api.IsAllowed(u, rbac.Actions.Project(projectId).Traces().View()) {
http.Error(w, "You are not allowed to view traces.", http.StatusForbidden)
return
}
case "logs":
if !api.IsAllowed(u, rbac.Actions.Project(projectId).Logs().View()) {
http.Error(w, "You are not allowed to view logs.", http.StatusForbidden)
return
}
case "costs":
if !api.IsAllowed(u, rbac.Actions.Project(projectId).Costs().View()) {
http.Error(w, "You are not allowed to view costs.", http.StatusForbidden)
return
}
case "risks":
if !api.IsAllowed(u, rbac.Actions.Project(projectId).Risks().View()) {
http.Error(w, "You are not allowed to view risks.", http.StatusForbidden)
return
}
}
world, project, cacheStatus, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if project == nil || world == nil {
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, nil))
return
}
chs := api.GetClickhouseClients(project)
defer chs.Close()
if chs.Error != nil {
klog.Warningln(chs.Error)
}
auditor.Audit(world, project, nil, nil)
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, views.Overview(r.Context(), chs, project, world, view, r.URL.Query().Get("query"))))
}
func (api *Api) Dashboards(w http.ResponseWriter, r *http.Request, u *db.User) {
world, project, cacheStatus, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if project == nil || world == nil {
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, nil))
return
}
vars := mux.Vars(r)
id := vars["dashboard"]
if r.Method == http.MethodPost {
if !api.IsAllowed(u, rbac.Actions.Project(string(project.Id)).Dashboards().Edit()) {
http.Error(w, "You are not allowed to configure dashboards.", http.StatusForbidden)
return
}
var form forms.DashboardForm
if err = forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "", http.StatusBadRequest)
return
}
switch form.Action {
case "create":
id, err = api.db.CreateDashboard(project.Id, form.Name, form.Description)
if err == nil {
http.Error(w, id, http.StatusCreated)
return
}
case "update":
err = api.db.UpdateDashboard(project.Id, id, form.Name, form.Description)
case "delete":
err = api.db.DeleteDashboard(project.Id, id)
default:
err = api.db.SaveDashboardConfig(project.Id, id, form.Dashboard.Config)
}
if err != nil {
klog.Errorf("failed to %s dashboard: %s", form.Action, err)
http.Error(w, "", http.StatusInternalServerError)
return
}
return
}
if id != "" {
dashboard, err := api.db.GetDashboard(project.Id, id)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
klog.Warningln("dashboard not found:", id)
http.Error(w, "Dashboard not found", http.StatusNotFound)
return
}
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if !api.IsAllowed(u, rbac.Actions.Project(string(project.Id)).Dashboard(dashboard.Name).View()) {
http.Error(w, "You are not allowed to view this dashboard.", http.StatusForbidden)
return
}
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, views.Dashboards.Dashboard(dashboard)))
return
}
dashboards, err := api.db.GetDashboards(project.Id)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, views.Dashboards.List(dashboards)))
}
func (api *Api) PanelData(w http.ResponseWriter, r *http.Request, u *db.User) {
projectId := db.ProjectId(mux.Vars(r)["project"])
project, err := api.db.GetProject(projectId)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
klog.Warningln("project not found:", projectId)
http.Error(w, "Project not found", http.StatusNotFound)
return
}
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
query := r.URL.Query().Get("query")
var config db.DashboardPanel
err = json.Unmarshal([]byte(query), &config)
if err != nil {
klog.Warningln("invalid query:", query)
http.Error(w, "Invalid query", http.StatusBadRequest)
return
}
promClients := map[string]prom.Client{}
extraSelectors := map[string]string{}
var maxRefreshInterval timeseries.Duration
if !project.Multicluster() {
promConfig := *project.PrometheusConfig(api.globalPrometheus)
extraSelectors[project.Name] = promConfig.ExtraSelector
promConfig.ExtraSelector = ""
promClient, err := prom.NewClient(&promConfig, project.ClickHouseConfig(api.globalClickHouse))
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
maxRefreshInterval = promConfig.RefreshInterval
defer promClient.Close()
promClients[project.Name] = promClient
} else {
projects, err := api.db.GetProjects()
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
for _, mp := range project.Settings.MemberProjects {
p := projects[mp]
if p == nil {
klog.Errorln("project not found:", mp)
http.Error(w, "", http.StatusInternalServerError)
return
}
promConfig := *p.PrometheusConfig(api.globalPrometheus)
extraSelectors[p.Name] = promConfig.ExtraSelector
promConfig.ExtraSelector = ""
promClient, err := prom.NewClient(&promConfig, p.ClickHouseConfig(api.globalClickHouse))
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
defer promClient.Close()
promClients[p.Name] = promClient
if promConfig.RefreshInterval > maxRefreshInterval {
maxRefreshInterval = promConfig.RefreshInterval
}
}
}
q := r.URL.Query()
from, to, _, _ := api.getTimeContext(db.ProjectId(mux.Vars(r)["project"]), q.Get("from"), q.Get("to"), q.Get("incident"), q.Get("alert"))
step := increaseStepForBigDurations(from, to, maxRefreshInterval)
data, err := views.Dashboards.PanelData(r.Context(), promClients, extraSelectors, config, from, to, step)
if err != nil {
klog.Errorln(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
utils.WriteJson(w, data)
}
func (api *Api) ApiKeys(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
project, err := api.db.GetProject(db.ProjectId(projectId))
if err != nil {
klog.Errorln("failed to get project:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
isAllowed := api.IsAllowed(u, rbac.Actions.Project(projectId).Settings().Edit())
if r.Method == http.MethodGet {
res := struct {
Editable bool `json:"editable"`
Keys []db.ApiKey `json:"keys"`
}{
Editable: isAllowed && !project.Settings.Readonly && !project.Multicluster(),
Keys: project.Settings.ApiKeys,
}
if !isAllowed {
for i := range res.Keys {
res.Keys[i].Key = ""
}
}
utils.WriteJson(w, res)
return
}
if !isAllowed {
http.Error(w, "You are not allowed to configure API keys.", http.StatusForbidden)
return
}
var form forms.ApiKeyForm
if err = forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "", http.StatusBadRequest)
return
}
switch form.Action {
case "generate":
form.Key = utils.RandomString(32)
project.Settings.ApiKeys = append(project.Settings.ApiKeys, form.ApiKey)
case "delete":
project.Settings.ApiKeys = slices.DeleteFunc(project.Settings.ApiKeys, func(k db.ApiKey) bool {
return k.Key == form.Key
})
case "edit":
for i, k := range project.Settings.ApiKeys {
if k.Key == form.Key {
project.Settings.ApiKeys[i].Description = form.Description
}
}
default:
return
}
if err = api.db.SaveProjectSettings(project); err != nil {
klog.Errorln("failed to save project api keys:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
}
func (api *Api) Inspections(w http.ResponseWriter, r *http.Request, u *db.User) {
world, project, cacheStatus, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if project == nil || world == nil {
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, nil))
return
}
checkConfigs := world.CheckConfigs
if checkConfigs == nil {
if checkConfigs, err = api.db.GetCheckConfigs(project.Id); err != nil {
klog.Errorln("failed to get check configs:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
}
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, views.Inspections(checkConfigs)))
}
func (api *Api) ApplicationCategories(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
project, err := api.db.GetProject(db.ProjectId(projectId))
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if r.Method == http.MethodPost {
if !api.IsAllowed(u, rbac.Actions.Project(projectId).ApplicationCategories().Edit()) {
http.Error(w, "You are not allowed to configure application categories.", http.StatusForbidden)
return
}
if project.Settings.Readonly {
http.Error(w, "This project is defined through the config and cannot be modified via the UI.", http.StatusForbidden)
return
}
var form forms.ApplicationCategoryForm
if err = forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "Invalid name or patterns", http.StatusBadRequest)
return
}
var category *db.ApplicationCategory
switch form.Action {
case "test":
err = form.SendTestNotification(r.Context(), project)
if err != nil {
klog.Warningln("failed to send test notification:", err)
http.Error(w, err.Error(), http.StatusBadRequest)
}
return
case "delete":
default:
category = &form.ApplicationCategory
}
if err = api.db.SaveApplicationCategory(project, form.Id, category); err != nil {
if errors.Is(err, db.ErrConflict) {
http.Error(w, "Application category already exists.", http.StatusConflict)
return
}
klog.Errorln("failed to save:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
return
}
categories := project.GetApplicationCategories()
if !r.URL.Query().Has("name") {
cs := maps.Values(categories)
sort.Slice(cs, func(i, j int) bool {
if cs[i].Builtin != cs[j].Builtin {
return cs[i].Builtin
}
return cs[i].Name < cs[j].Name
})
utils.WriteJson(w, cs)
return
}
name := model.ApplicationCategory(r.URL.Query().Get("name"))
if name == "" {
category := project.NewApplicationCategory()
utils.WriteJson(w, forms.ApplicationCategoryForm{ApplicationCategory: *category})
return
}
category := categories[name]
if category == nil {
klog.Warningln("unknown application category:", name)
http.Error(w, "Unknown application category: "+string(name), http.StatusNotFound)
return
}
utils.WriteJson(w, forms.ApplicationCategoryForm{Id: category.Name, ApplicationCategory: *category})
}
func (api *Api) CustomApplications(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
project, err := api.db.GetProject(db.ProjectId(projectId))
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if r.Method == http.MethodPost {
if !api.IsAllowed(u, rbac.Actions.Project(projectId).CustomApplications().Edit()) {
http.Error(w, "You are not allowed to configure custom applications.", http.StatusForbidden)
return
}
if project.Settings.Readonly {
http.Error(w, "This project is defined through the config and cannot be modified via the UI.", http.StatusForbidden)
return
}
var form forms.CustomApplicationForm
if err = forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "Invalid name or patterns", http.StatusBadRequest)
return
}
if err = api.db.SaveCustomApplication(project.Id, form.Name, form.NewName, form.InstancePatterns); err != nil {
klog.Errorln("failed to save:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
return
}
utils.WriteJson(w, views.CustomApplications(project))
}
func (api *Api) CustomCloudPricing(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
p, err := api.db.GetProject(db.ProjectId(projectId))
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if r.Method == http.MethodGet {
utils.WriteJson(w, p.Settings.CustomCloudPricing)
return
}
if !api.IsAllowed(u, rbac.Actions.Project(projectId).CustomCloudPricing().Edit()) {
http.Error(w, "You are not allowed to configure custom cloud pricing.", http.StatusForbidden)
return
}
switch r.Method {
case http.MethodDelete:
p.Settings.CustomCloudPricing = nil
case http.MethodPost:
var form forms.CustomCloudPricingForm
if err := forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "Invalid form", http.StatusBadRequest)
return
}
p.Settings.CustomCloudPricing = &form.CustomCloudPricing
default:
http.Error(w, "", http.StatusMethodNotAllowed)
return
}
if err := api.db.SaveProjectSettings(p); err != nil {
klog.Errorln("failed to save:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
}
func (api *Api) Integrations(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
if r.Method == http.MethodPut {
if !api.IsAllowed(u, rbac.Actions.Project(projectId).Integrations().Edit()) {
http.Error(w, "You are not allowed to configure notification integrations.", http.StatusForbidden)
return
}
var form forms.IntegrationsForm
if err := forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "Invalid base url", http.StatusBadRequest)
return
}
if err := api.db.SaveIntegrationsBaseUrl(db.ProjectId(projectId), form.BaseUrl); err != nil {
klog.Errorln("failed to save:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
return
}
project, err := api.db.GetProject(db.ProjectId(projectId))
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
integrations := project.Settings.Integrations
utils.WriteJson(w, struct {
BaseUrl string `json:"base_url"`
Integrations []db.IntegrationInfo `json:"integrations"`
Readonly bool `json:"readonly"`
}{
BaseUrl: integrations.BaseUrl,
Integrations: integrations.GetInfo(),
Readonly: integrations.Readonly,
})
}
func (api *Api) Integration(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
project, err := api.db.GetProject(db.ProjectId(projectId))
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
t := db.IntegrationType(vars["type"])
if t == db.IntegrationTypeGCP || t == db.IntegrationTypeOCI {
if r.Method != http.MethodGet {
http.Error(w, "this integration is configured in the Coroot custom resource or the cluster-agent config file", http.StatusMethodNotAllowed)
return
}
world, _, _, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
}
view := views.GCP(world)
if t == db.IntegrationTypeOCI {
view = views.OCI(world)
}
utils.WriteJson(w, struct {
View any `json:"view"`
}{View: view})
return
}
form := forms.NewIntegrationForm(t, api.globalClickHouse, api.globalPrometheus)
if form == nil {
klog.Warningln("unknown integration type:", t)
http.Error(w, "", http.StatusBadRequest)
return
}
isAllowed := api.IsAllowed(u, rbac.Actions.Project(projectId).Integrations().Edit())
if r.Method == http.MethodGet {
form.Get(project, !isAllowed)
switch t {
case db.IntegrationTypeAWS:
world, _, _, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
}
utils.WriteJson(w, struct {
Form forms.IntegrationForm `json:"form"`
View any `json:"view"`
}{
Form: form,
View: views.AWS(world, project.Settings.Integrations.AWS != nil),
})
case db.IntegrationTypeClickhouse:
cfg := project.ClickHouseConfig(api.globalClickHouse)
var ci *clickhouse.ClusterInfo
if cfg != nil && cfg.Protocol != ch.ProtocolCoroot {
var err error
if ci, err = clickhouse.GetClusterInfo(r.Context(), cfg, project); err != nil {
klog.Errorln(err)
}
}
utils.WriteJson(w, struct {
Form forms.IntegrationForm `json:"form"`
ClusterInfo *clickhouse.ClusterInfo `json:"cluster_info"`
}{
Form: form,
ClusterInfo: ci,
})
default:
utils.WriteJson(w, form)
}
return
}
if !isAllowed {
http.Error(w, "You are not allowed to configure integrations.", http.StatusForbidden)
return
}
switch r.Method {
case http.MethodPost, http.MethodPut:
if err := forms.ReadAndValidate(r, form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "invalid data", http.StatusBadRequest)
return
}
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
switch r.Method {
case http.MethodPost:
if err = form.Test(ctx, project); err != nil {
klog.Errorln("failed to test:", err)
http.Error(w, err.Error(), http.StatusBadRequest)
}
return
case http.MethodPut:
if err = form.Update(ctx, project, false); err != nil {
klog.Errorln("failed to update:", err)
http.Error(w, err.Error(), http.StatusBadRequest)
}
case http.MethodDelete:
if err = form.Update(ctx, project, true); err != nil {
klog.Errorln("failed to delete:", err)
http.Error(w, err.Error(), http.StatusBadRequest)
}
default:
http.Error(w, "", http.StatusMethodNotAllowed)
return
}
if err := api.db.SaveProjectIntegration(project, t); err != nil {
klog.Errorln("failed to save:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if api.globalClickHouse == nil {
err = api.collector.UpdateClickhouseClient(r.Context(), project)
if err != nil {
klog.Errorln("clickhouse error:", err)
http.Error(w, "Clickhouse error: "+err.Error(), http.StatusInternalServerError)
return
}
}
}
func (api *Api) Prom(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
project, err := api.db.GetProject(db.ProjectId(projectId))
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
var c prom.Client
if !project.Multicluster() {
c, err = prom.NewClient(project.PrometheusConfig(api.globalPrometheus), project.ClickHouseConfig(api.globalClickHouse))
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
defer c.Close()
} else {
ds := r.Header.Get("X-Datasource")
if ds == "" {
klog.Warningln("datasource is not set")
http.Error(w, "", http.StatusBadRequest)
return
}
projects, err := api.db.GetProjects()
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
p, ok := projects[ds]
if !ok {
klog.Warningln("project not found:", ds)
http.Error(w, "", http.StatusBadRequest)
return
}
c, err = prom.NewClient(p.PrometheusConfig(api.globalPrometheus), p.ClickHouseConfig(api.globalClickHouse))
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
defer c.Close()
}
rest := vars["rest"]
switch rest {
case "series":
c.Series(r, w)
case "metadata":
c.MetricMetadata(r, w)
default:
parts := strings.Split(rest, "/")
var labelName string
if len(parts) == 3 && parts[0] == "label" && parts[2] == "values" {
labelName = parts[1]
c.LabelValues(r, w, labelName)
} else {
http.Error(w, "", http.StatusNotFound)
}
}
}
func (api *Api) Application(w http.ResponseWriter, r *http.Request, u *db.User) {
projectId := mux.Vars(r)["project"]
appId, err := GetApplicationId(r)
if err != nil {
klog.Warningln(err)
http.Error(w, "invalid application id", http.StatusBadRequest)
return
}
world, project, cacheStatus, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if project == nil || world == nil {
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, nil))
return
}
app := world.GetApplication(appId)
if app == nil {
klog.Warningln("application not found:", appId)
http.Error(w, "Application not found", http.StatusNotFound)
return
}
if !api.IsAllowed(u, rbac.Actions.Project(projectId).Application(app.Category, app.Id.Namespace, app.Id.Kind, app.Id.Name).View()) {
http.Error(w, "You are not allowed to view this application.", http.StatusForbidden)
return
}
loadDbChangeEvents(r.Context(), api, project, app, world)
auditor.Audit(world, project, app, nil)
app.AddReport(model.AuditReportProfiling, &model.Widget{Profiling: &model.Profiling{ApplicationId: app.Id}, Width: "100%"})
app.AddReport(model.AuditReportTracing, &model.Widget{Tracing: &model.Tracing{ApplicationId: app.Id}, Width: "100%"})
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, views.Application(project, world, app)))
}
func (api *Api) Incidents(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := db.ProjectId(vars["project"])
limit := 100
if l := r.URL.Query().Get("limit"); l != "" {
l64, err := strconv.ParseUint(l, 10, 32)
if err != nil {
klog.Warningln("invalid limit:", l)
http.Error(w, "", http.StatusBadRequest)
return
}
limit = int(l64)
}
project, err := api.db.GetProject(projectId)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
http.Error(w, "project not found", http.StatusNotFound)
klog.Warningln("project not found:", projectId)
return
}
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
incidents, err := api.db.GetIncidentsForList(project.Id, limit)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
world, project, cacheStatus, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if project == nil || world == nil {
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, nil))
return
}
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, views.Incidents(world, incidents)))
}
func (api *Api) Incident(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
incidentKey := vars["incident"]
incident, err := api.db.GetIncidentByKey(db.ProjectId(projectId), incidentKey)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
klog.Warningln("incident not found:", vars["key"])
http.Error(w, "Incident not found", http.StatusNotFound)
return
}
klog.Warningln("failed to get incident:", err)
http.Error(w, "failed to get incident", http.StatusInternalServerError)
return
}
values := r.URL.Query()
values.Add("incident", incidentKey)
r.URL.RawQuery = values.Encode()
world, project, cacheStatus, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if project == nil || world == nil {
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, nil))
return
}
app := world.GetApplication(incident.ApplicationId)
if app == nil {
klog.Warningln("application not found:", incident.ApplicationId)
http.Error(w, "Application not found", http.StatusNotFound)
return
}
if !api.IsAllowed(u, rbac.Actions.Project(projectId).Application(app.Category, app.Id.Namespace, app.Id.Kind, app.Id.Name).View()) {
http.Error(w, "You are not allowed to view this application.", http.StatusForbidden)
return
}
auditor.Audit(world, project, app, nil)
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, views.Incident(world, app, incident)))
}
func (api *Api) Alerts(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := db.ProjectId(vars["project"])
if !api.IsAllowed(u, rbac.Actions.Project(string(projectId)).Alerts().View()) {
http.Error(w, "", http.StatusForbidden)
return
}
query := db.AlertsQuery{
IncludeResolved: r.URL.Query().Get("include_resolved") == "true",
Search: r.URL.Query().Get("search"),
SortBy: r.URL.Query().Get("sort_by"),
SortDesc: r.URL.Query().Get("sort_desc") != "false", // default to desc
Limit: 50,
Offset: 0,
}
if l := r.URL.Query().Get("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
query.Limit = parsed
}
}
if o := r.URL.Query().Get("offset"); o != "" {
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
query.Offset = parsed
}
}
if query.SortBy == "cluster" {
projectNames, err := api.db.GetProjectNames()
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
query.ClusterNames = make(map[string]string, len(projectNames))
for id, name := range projectNames {
query.ClusterNames[string(id)] = name
}
}
result, err := api.db.QueryAlerts(projectId, query)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
rules, err := api.db.GetAlertingRules(projectId)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
alertIds := make([]string, 0, len(result.Alerts))
for _, a := range result.Alerts {
alertIds = append(alertIds, a.Id)
}
notifications, err := api.db.GetAlertNotificationsByAlertIds(projectId, alertIds)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
world, project, cacheStatus, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if project == nil || world == nil {
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, nil))
return
}
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, views.Alerts(world, result, rules, notifications)))
}
func (api *Api) Alert(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := db.ProjectId(vars["project"])
alertId := vars["alert"]
if !api.IsAllowed(u, rbac.Actions.Project(string(projectId)).Alerts().View()) {
http.Error(w, "", http.StatusForbidden)
return
}
a, err := api.db.GetAlert(projectId, alertId)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
http.Error(w, "Alert not found", http.StatusNotFound)
return
}
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
rules, err := api.db.GetAlertingRules(projectId)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
notifications, err := api.db.GetAlertNotificationsByAlertIds(projectId, []string{alertId})
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
world, project, cacheStatus, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if project == nil {
utils.WriteJson(w, api.WithContext(nil, nil, nil, nil))
return
}
app := world.GetApplication(a.ApplicationId)
if app != nil {
if !api.IsAllowed(u, rbac.Actions.Project(string(projectId)).Application(app.Category, app.Id.Namespace, app.Id.Kind, app.Id.Name).View()) {
http.Error(w, "You are not allowed to view this application.", http.StatusForbidden)
return
}
auditor.Audit(world, project, app, nil)
}
chs := api.GetClickhouseClients(project)
defer chs.Close()
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, views.Alert(world, a, app, rules, notifications[alertId], chs)))
}
func (api *Api) ResolveAlerts(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
if !api.IsAllowed(u, rbac.Actions.Project(projectId).Alerts().Edit()) {
http.Error(w, "", http.StatusForbidden)
return
}
var req struct {
Ids []string `json:"ids"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
resolvedBy := u.Name
if resolvedBy == "" {
resolvedBy = u.Email
}
project, err := api.db.GetProject(db.ProjectId(projectId))
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if _, err := api.resolveAlerts(project, req.Ids, resolvedBy); err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (api *Api) resolveAlerts(project *db.Project, ids []string, resolvedBy string) (int, error) {
var toNotify []*model.Alert
for _, id := range ids {
a, err := api.db.GetAlert(project.Id, id)
if err != nil {
continue
}
if a.ResolvedAt == 0 {
toNotify = append(toNotify, a)
}
}
if err := api.db.ResolveAlerts(project.Id, ids, resolvedBy); err != nil {
return 0, err
}
if len(toNotify) == 0 {
return 0, nil
}
rulesMap := map[string]*model.AlertingRule{}
for _, a := range toNotify {
a.ResolvedBy = resolvedBy
rule := rulesMap[a.RuleId]
if rule == nil {
rule, _ = api.db.GetAlertingRule(project.Id, model.AlertingRuleId(a.RuleId))
rulesMap[a.RuleId] = rule
}
if rule != nil {
notifications.EnqueueResolvedAlerts(api.db, project, []*model.Alert{a}, rule)
}
}
return len(toNotify), nil
}
func (api *Api) SuppressAlerts(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
if !api.IsAllowed(u, rbac.Actions.Project(projectId).Alerts().Edit()) {
http.Error(w, "", http.StatusForbidden)
return
}
var req struct {
Ids []string `json:"ids"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
if len(req.Ids) == 0 {
http.Error(w, "no alert ids provided", http.StatusBadRequest)
return
}
var alertsToNotify []*model.Alert
for _, id := range req.Ids {
alert, err := api.db.GetAlert(db.ProjectId(projectId), id)
if err != nil {
continue
}
if alert.ResolvedAt == 0 && alert.ManuallyResolvedAt == 0 && !alert.Suppressed {
alertsToNotify = append(alertsToNotify, alert)
}
}
suppressedBy := u.Name
if suppressedBy == "" {
suppressedBy = u.Email
}
if err := api.db.SuppressAlerts(db.ProjectId(projectId), req.Ids, suppressedBy); err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if len(alertsToNotify) > 0 {
project, err := api.db.GetProject(db.ProjectId(projectId))
if err != nil {
klog.Errorln("failed to get project for notifications:", err)
} else {
rulesMap := make(map[string]*model.AlertingRule)
for _, alert := range alertsToNotify {
alert.ResolvedBy = suppressedBy
rule := rulesMap[alert.RuleId]
if rule == nil {
rule, _ = api.db.GetAlertingRule(db.ProjectId(projectId), model.AlertingRuleId(alert.RuleId))
rulesMap[alert.RuleId] = rule
}
if rule != nil {
notifications.EnqueueResolvedAlerts(api.db, project, []*model.Alert{alert}, rule)
}
}
}
}
w.WriteHeader(http.StatusNoContent)
}
func (api *Api) ReopenAlerts(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
if !api.IsAllowed(u, rbac.Actions.Project(projectId).Alerts().Edit()) {
http.Error(w, "", http.StatusForbidden)
return
}
var req struct {
Ids []string `json:"ids"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
if len(req.Ids) == 0 {
http.Error(w, "no alert ids provided", http.StatusBadRequest)
return
}
_, err := api.db.ReopenAlerts(db.ProjectId(projectId), req.Ids)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (api *Api) AlertingRules(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
switch r.Method {
case http.MethodGet:
if !api.IsAllowed(u, rbac.Actions.Project(projectId).AlertingRules().View()) {
http.Error(w, "", http.StatusForbidden)
return
}
rules, err := api.db.GetAlertingRules(db.ProjectId(projectId))
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
alertCounts, err := api.db.GetFiringAlertCountsByRule(db.ProjectId(projectId))
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
world, project, cacheStatus, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if project == nil || world == nil {
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, nil))
return
}
type checkOption struct {
Id model.CheckId `json:"id"`
Title string `json:"title"`
}
var checks []checkOption
for id, cfg := range model.GetCheckConfigs() {
checks = append(checks, checkOption{Id: id, Title: cfg.Title})
}
type categoryOption struct {
Name model.ApplicationCategory `json:"name"`
}
var categories []categoryOption
for name := range project.GetApplicationCategories() {
categories = append(categories, categoryOption{Name: name})
}
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, map[string]any{"rules": rules, "checks": checks, "categories": categories, "alert_counts": alertCounts}))
case http.MethodPost:
if !api.IsAllowed(u, rbac.Actions.Project(projectId).AlertingRules().Edit()) {
http.Error(w, "", http.StatusForbidden)
return
}
var rule model.AlertingRule
if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
rule.Id = model.AlertingRuleId(utils.NanoId(8))
rule.ProjectId = projectId
rule.Builtin = false
if err := api.db.CreateAlertingRule(db.ProjectId(projectId), &rule); err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
utils.WriteJson(w, rule)
}
}
func (api *Api) AlertingRule(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
ruleId := model.AlertingRuleId(vars["rule"])
switch r.Method {
case http.MethodGet:
if !api.IsAllowed(u, rbac.Actions.Project(projectId).AlertingRules().View()) {
http.Error(w, "", http.StatusForbidden)
return
}
rule, err := api.db.GetAlertingRule(db.ProjectId(projectId), ruleId)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
http.Error(w, "Rule not found", http.StatusNotFound)
return
}
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
utils.WriteJson(w, rule)
case http.MethodPut:
if !api.IsAllowed(u, rbac.Actions.Project(projectId).AlertingRules().Edit()) {
http.Error(w, "", http.StatusForbidden)
return
}
existing, err := api.db.GetAlertingRule(db.ProjectId(projectId), ruleId)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
http.Error(w, "Rule not found", http.StatusNotFound)
return
}
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if existing.Readonly {
http.Error(w, "This rule is managed via config and cannot be edited", http.StatusForbidden)
return
}
var rule model.AlertingRule
if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
rule.Id = ruleId
rule.ProjectId = projectId
if err := api.db.UpdateAlertingRule(db.ProjectId(projectId), &rule); err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
utils.WriteJson(w, rule)
case http.MethodDelete:
if !api.IsAllowed(u, rbac.Actions.Project(projectId).AlertingRules().Edit()) {
http.Error(w, "", http.StatusForbidden)
return
}
rule, err := api.db.GetAlertingRule(db.ProjectId(projectId), ruleId)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
http.Error(w, "Rule not found or is builtin", http.StatusNotFound)
return
}
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if rule.Readonly {
http.Error(w, "This rule is managed via config and cannot be deleted", http.StatusForbidden)
return
}
if resolvedAlerts, err := api.db.ResolveAlertsByRule(db.ProjectId(projectId), string(ruleId)); err != nil {
klog.Errorln(err)
} else if len(resolvedAlerts) > 0 {
if project, err := api.db.GetProject(db.ProjectId(projectId)); err != nil {
klog.Errorln(err)
} else {
notifications.EnqueueResolvedAlerts(api.db, project, resolvedAlerts, rule)
}
}
if err := api.db.DeleteAlertingRule(db.ProjectId(projectId), ruleId); err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
}
func (api *Api) AlertingRulesExport(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
if !api.IsAllowed(u, rbac.Actions.Project(projectId).AlertingRules().View()) {
http.Error(w, "", http.StatusForbidden)
return
}
rules, err := api.db.GetAlertingRules(db.ProjectId(projectId))
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
var exported []config.AlertingRule
for _, r := range rules {
severity := r.Severity.String()
er := config.AlertingRule{
Id: string(r.Id),
Name: &r.Name,
Severity: &severity,
For: &r.For,
KeepFiringFor: &r.KeepFiringFor,
}
if r.Source.Type != "" {
er.Source = &r.Source
}
if r.Selector.Type != "" {
er.Selector = &r.Selector
}
if r.Templates.Summary != "" || r.Templates.Description != "" {
er.Templates = &r.Templates
}
if r.NotificationCategory != "" {
er.NotificationCategory = &r.NotificationCategory
}
if !r.Enabled {
enabled := false
er.Enabled = &enabled
}
exported = append(exported, er)
}
out, err := yaml.Marshal(exported)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
utils.WriteJson(w, map[string]string{"yaml": string(out)})
}
func (api *Api) appConfigProjectId(project *db.Project, appId model.ApplicationId) (db.ProjectId, bool) {
if !project.Multicluster() || appId.ClusterId == "" {
return project.Id, true
}
projects, err := api.db.GetProjects()
if err != nil {
klog.Errorln("failed to get projects:", err)
return "", false
}
for _, mp := range project.Settings.MemberProjects {
p := projects[mp]
if p == nil {
continue
}
if p.ClusterId() == appId.ClusterId {
return p.Id, true
}
}
return "", false
}
func (api *Api) Inspection(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
appId, err := GetApplicationId(r)
if err != nil {
klog.Warningln(err)
http.Error(w, "invalid application id", http.StatusBadRequest)
return
}
checkId := model.CheckId(vars["type"])
world, project, _, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if world == nil {
http.Error(w, "Application not found", http.StatusNotFound)
return
}
configProjectId, ok := api.appConfigProjectId(project, appId)
if !ok {
klog.Warningln("application doesn't belong to any member project:", appId)
http.Error(w, "Application not found", http.StatusNotFound)
return
}
var app *model.Application
var category model.ApplicationCategory
if !appId.IsZero() {
app = world.GetApplication(appId)
if app != nil {
category = app.Category
} else {
category = project.CalcApplicationCategory(appId)
}
}
switch r.Method {
case http.MethodGet:
checkConfigs, err := api.db.GetCheckConfigs(configProjectId)
if err != nil {
klog.Errorln("failed to get check configs:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
type Integration struct {
Name string `json:"name"`
Details string `json:"details"`
}
res := struct {
Form any `json:"form"`
Integrations []Integration `json:"integrations"`
}{}
if app != nil {
if categorySettings := project.GetApplicationCategories()[app.Category]; categorySettings != nil {
notificationSettings := categorySettings.NotificationSettings.Incidents
if notificationSettings.Enabled {
if slack := notificationSettings.Slack; slack != nil && slack.Enabled {
res.Integrations = append(res.Integrations, Integration{Name: "Slack", Details: "channel: #" + slack.Channel})
}
if teams := notificationSettings.Teams; teams != nil && teams.Enabled {
res.Integrations = append(res.Integrations, Integration{Name: "MS Teams"})
}
if pagerduty := notificationSettings.Pagerduty; pagerduty != nil && pagerduty.Enabled {
res.Integrations = append(res.Integrations, Integration{Name: "Pagerduty"})
}
if opsgenie := notificationSettings.Opsgenie; opsgenie != nil && opsgenie.Enabled {
res.Integrations = append(res.Integrations, Integration{Name: "Opsgenie"})
}
if webhook := notificationSettings.Webhook; webhook != nil && webhook.Enabled {
res.Integrations = append(res.Integrations, Integration{Name: "Webhook"})
}
}
}
}
switch checkId {
case model.Checks.SLOAvailability.Id:
cfg, def := checkConfigs.GetAvailability(appId)
res.Form = forms.CheckConfigSLOAvailabilityForm{Configs: []model.CheckConfigSLOAvailability{cfg}, Default: def}
case model.Checks.SLOLatency.Id:
cfg, def := checkConfigs.GetLatency(appId, category)
res.Form = forms.CheckConfigSLOLatencyForm{Configs: []model.CheckConfigSLOLatency{cfg}, Default: def}
case model.Checks.SLOLLMLatency.Id:
cfg, def := checkConfigs.GetLLMLatency(appId)
res.Form = forms.CheckConfigSLOLatencyForm{Configs: []model.CheckConfigSLOLatency{cfg}, Default: def}
default:
form := forms.CheckConfigForm{
Configs: checkConfigs.GetSimpleAll(checkId, appId),
}
if len(form.Configs) == 0 {
http.Error(w, "", http.StatusNotFound)
return
}
res.Form = form
}
utils.WriteJson(w, res)
return
case http.MethodPost:
if !api.IsAllowed(u, rbac.Actions.Project(projectId).Inspections().Edit()) {
http.Error(w, "You are not allowed to configure inspections.", http.StatusForbidden)
return
}
switch checkId {
case model.Checks.SLOAvailability.Id:
var form forms.CheckConfigSLOAvailabilityForm
if err := forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "", http.StatusBadRequest)
return
}
if err := form.Validate(); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err = api.db.SaveCheckConfig(configProjectId, appId, checkId, form.Configs); err != nil {
klog.Errorln("failed to save check config:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
case model.Checks.SLOLatency.Id, model.Checks.SLOLLMLatency.Id:
var form forms.CheckConfigSLOLatencyForm
if err = forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "", http.StatusBadRequest)
return
}
if err := form.Validate(); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err = api.db.SaveCheckConfig(configProjectId, appId, checkId, form.Configs); err != nil {
klog.Errorln("failed to save check config:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
default:
var form forms.CheckConfigForm
if err := forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "", http.StatusBadRequest)
return
}
for level, cfg := range form.Configs {
var id model.ApplicationId
switch level {
case 0:
continue
case 1:
id = model.ApplicationIdZero
case 2:
id = appId
}
if err = api.db.SaveCheckConfig(configProjectId, id, checkId, cfg); err != nil {
klog.Errorln("failed to save check config:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
}
return
}
}
}
func (api *Api) Instrumentation(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
appId, err := GetApplicationId(r)
if err != nil {
klog.Warningln(err)
http.Error(w, "invalid application id", http.StatusBadRequest)
return
}
world, project, _, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if world == nil {
http.Error(w, "Application not found", http.StatusNotFound)
return
}
app := world.GetApplication(appId)
if app == nil {
klog.Warningln("application not found:", appId)
http.Error(w, "Application not found", http.StatusNotFound)
return
}
isAllowed := api.IsAllowed(u, rbac.Actions.Project(projectId).Instrumentations().Edit())
if r.Method == http.MethodPost {
if !isAllowed {
http.Error(w, "You are not allowed to configure database integrations.", http.StatusForbidden)
return
}
configProjectId, ok := api.appConfigProjectId(project, appId)
if !ok {
klog.Warningln("application doesn't belong to any member project:", appId)
http.Error(w, "Application not found", http.StatusNotFound)
return
}
var form forms.ApplicationInstrumentationForm
if err = forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "invalid data", http.StatusBadRequest)
return
}
if err = api.db.SaveApplicationSetting(configProjectId, appId, &form.ApplicationInstrumentation); err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
return
}
t := model.ApplicationType(vars["type"]).InstrumentationType()
var instrumentation *model.ApplicationInstrumentation
if app.Settings != nil && app.Settings.Instrumentation != nil && app.Settings.Instrumentation[t] != nil {
instrumentation = app.Settings.Instrumentation[t]
if instrumentation.Enabled == nil {
instrumentation.Enabled = utils.Ptr(true)
}
} else {
instrumentation = model.GetDefaultInstrumentation(t)
instrumentation.Enabled = utils.Ptr(false)
}
if instrumentation == nil {
http.Error(w, fmt.Sprintf("unsupported instrumentation type: %s", t), http.StatusBadRequest)
return
}
if !isAllowed {
instrumentation.Credentials.Username = "<hidden>"
instrumentation.Credentials.Password = "<hidden>"
}
utils.WriteJson(w, instrumentation)
}
func (api *Api) Profiling(w http.ResponseWriter, r *http.Request, u *db.User) {
projectId := mux.Vars(r)["project"]
appId, err := GetApplicationId(r)
if err != nil {
klog.Warningln(err)
http.Error(w, "invalid application id", http.StatusBadRequest)
return
}
if r.Method == http.MethodPost {
if !api.IsAllowed(u, rbac.Actions.Project(projectId).Inspections().Edit()) {
http.Error(w, "You are not allowed to configure profiling settings.", http.StatusForbidden)
return
}
project, err := api.db.GetProject(db.ProjectId(projectId))
if err != nil {
if errors.Is(err, db.ErrNotFound) {
http.Error(w, "Project not found.", http.StatusNotFound)
return
}
klog.Errorln("failed to get project:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
configProjectId, ok := api.appConfigProjectId(project, appId)
if !ok {
klog.Warningln("application doesn't belong to any member project:", appId)
http.Error(w, "Application not found", http.StatusNotFound)
return
}
var form forms.ApplicationSettingsProfilingForm
if err := forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "invalid data", http.StatusBadRequest)
return
}
if err := api.db.SaveApplicationSetting(configProjectId, appId, &form.ApplicationSettingsProfiling); err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
return
}
world, project, cacheStatus, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if project == nil || world == nil {
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, nil))
return
}
app := world.GetApplication(appId)
if app == nil {
klog.Warningln("application not found:", appId)
http.Error(w, "Application not found", http.StatusNotFound)
return
}
var ch *clickhouse.Client
if ch, err = api.GetClickhouseClient(project, app.Id.ClusterId); err != nil {
klog.Warningln(err)
http.Error(w, "ClickHouse is not available", http.StatusInternalServerError)
return
}
defer ch.Close()
q := r.URL.Query()
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, views.Profiling(r.Context(), ch, app, q, world)))
}
func (api *Api) Tracing(w http.ResponseWriter, r *http.Request, u *db.User) {
projectId := mux.Vars(r)["project"]
appId, err := GetApplicationId(r)
if err != nil {
klog.Warningln(err)
http.Error(w, "invalid application id", http.StatusBadRequest)
return
}
if r.Method == http.MethodPost {
if !api.IsAllowed(u, rbac.Actions.Project(projectId).Inspections().Edit()) {
http.Error(w, "You are not allowed to configure tracing settings.", http.StatusForbidden)
return
}
project, err := api.db.GetProject(db.ProjectId(projectId))
if err != nil {
if errors.Is(err, db.ErrNotFound) {
http.Error(w, "Project not found.", http.StatusNotFound)
return
}
klog.Errorln("failed to get project:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
configProjectId, ok := api.appConfigProjectId(project, appId)
if !ok {
klog.Warningln("application doesn't belong to any member project:", appId)
http.Error(w, "Application not found", http.StatusNotFound)
return
}
var form forms.ApplicationSettingsTracingForm
if err := forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "invalid data", http.StatusBadRequest)
return
}
if err := api.db.SaveApplicationSetting(configProjectId, appId, &form.ApplicationSettingsTracing); err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
return
}
world, project, cacheStatus, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if project == nil || world == nil {
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, nil))
return
}
app := world.GetApplication(appId)
if app == nil {
klog.Warningln("application not found:", appId)
http.Error(w, "Application not found", http.StatusNotFound)
return
}
q := r.URL.Query()
var ch *clickhouse.Client
if ch, err = api.GetClickhouseClient(project, app.Id.ClusterId); err != nil {
klog.Warningln(err)
http.Error(w, "ClickHouse is not available", http.StatusInternalServerError)
return
}
defer ch.Close()
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, views.Tracing(r.Context(), ch, app, q, world)))
}
func (api *Api) Logs(w http.ResponseWriter, r *http.Request, u *db.User) {
projectId := mux.Vars(r)["project"]
appId, err := GetApplicationId(r)
if err != nil {
klog.Warningln(err)
http.Error(w, "invalid application id", http.StatusBadRequest)
return
}
if r.Method == http.MethodPost {
if !api.IsAllowed(u, rbac.Actions.Project(projectId).Inspections().Edit()) {
http.Error(w, "You are not allowed to configure logs settings.", http.StatusForbidden)
return
}
project, err := api.db.GetProject(db.ProjectId(projectId))
if err != nil {
if errors.Is(err, db.ErrNotFound) {
http.Error(w, "Project not found.", http.StatusNotFound)
return
}
klog.Errorln("failed to get project:", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
configProjectId, ok := api.appConfigProjectId(project, appId)
if !ok {
klog.Warningln("application doesn't belong to any member project:", appId)
http.Error(w, "Application not found", http.StatusNotFound)
return
}
var form forms.ApplicationSettingsLogsForm
if err := forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "invalid data", http.StatusBadRequest)
return
}
if err := api.db.SaveApplicationSetting(configProjectId, appId, &form.ApplicationSettingsLogs); err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
return
}
world, project, cacheStatus, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if project == nil || world == nil {
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, nil))
return
}
app := world.GetApplication(appId)
if app == nil {
klog.Warningln("application not found:", appId)
http.Error(w, "Application not found", http.StatusNotFound)
return
}
ch, chErr := api.GetClickhouseClient(project, app.Id.ClusterId)
if chErr != nil {
klog.Warningln(chErr)
}
defer ch.Close()
q := r.URL.Query()
res := views.Logs(r.Context(), ch, app, q, world)
if chErr != nil {
res.Message = "Failed to load logs: ClickHouse is not available"
}
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, res))
}
func (api *Api) Risks(w http.ResponseWriter, r *http.Request, u *db.User) {
projectId := mux.Vars(r)["project"]
appId, err := GetApplicationId(r)
if err != nil {
klog.Warningln(err)
http.Error(w, "invalid application id", http.StatusBadRequest)
return
}
world, project, cacheStatus, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if project == nil || world == nil {
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, nil))
return
}
app := world.GetApplication(appId)
if app == nil {
klog.Warningln("application not found:", appId)
http.Error(w, "Application not found", http.StatusNotFound)
return
}
if r.Method == http.MethodPost {
if !api.IsAllowed(u, rbac.Actions.Project(projectId).Risks().Edit()) {
http.Error(w, "You are not allowed to dismiss risks.", http.StatusForbidden)
return
}
var form forms.ApplicationSettingsRisksForm
if err := forms.ReadAndValidate(r, &form); err != nil {
klog.Warningln("bad request:", err)
http.Error(w, "invalid data", http.StatusBadRequest)
return
}
var overrides []model.RiskOverride
switch form.Action {
case "dismiss":
newRo := model.RiskOverride{
Key: form.Key,
Dismissal: &model.RiskDismissal{
By: u.Name,
Timestamp: time.Now().Unix(),
Reason: form.Reason,
},
}
if app.Settings != nil {
for _, ro := range app.Settings.RiskOverrides {
if ro.Key != form.Key {
overrides = append(overrides, ro)
}
}
}
overrides = append(overrides, newRo)
case "mark_as_active":
if app.Settings != nil {
for _, ro := range app.Settings.RiskOverrides {
if ro.Key != form.Key {
overrides = append(overrides, ro)
}
}
}
default:
klog.Warningln("unknown risk action:", form.Action)
http.Error(w, "", http.StatusBadRequest)
return
}
configProjectId, ok := api.appConfigProjectId(project, appId)
if !ok {
klog.Warningln("application doesn't belong to any member project:", appId)
http.Error(w, "Application not found", http.StatusNotFound)
return
}
if err = api.db.SaveApplicationSetting(configProjectId, appId, overrides); err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
return
}
}
func (api *Api) Node(w http.ResponseWriter, r *http.Request, u *db.User) {
vars := mux.Vars(r)
projectId := vars["project"]
nodeName, err := url.QueryUnescape(vars["node"])
if err != nil {
klog.Warningln(err)
http.Error(w, "invalid node name", http.StatusBadRequest)
return
}
parts := strings.SplitN(nodeName, ":", 2)
if len(parts) == 2 {
nodeName = parts[1]
}
if !api.IsAllowed(u, rbac.Actions.Project(projectId).Node(nodeName).View()) {
http.Error(w, "You are not allowed to view this node.", http.StatusForbidden)
return
}
world, project, cacheStatus, err := api.LoadWorldByRequest(r)
if err != nil {
klog.Errorln(err)
http.Error(w, "", http.StatusInternalServerError)
return
}
if project == nil || world == nil {
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, nil))
return
}
node := world.GetNode(nodeName)
if node == nil {
klog.Warningf("node not found: %s ", nodeName)
http.Error(w, "Node not found", http.StatusNotFound)
return
}
auditor.Audit(world, project, nil, nil)
utils.WriteJson(w, api.WithContext(project, cacheStatus, world, auditor.AuditNode(world, node)))
}
func (api *Api) LoadWorld(ctx context.Context, project *db.Project, from, to timeseries.Time) (*model.World, *cache.Status, error) {
if api.loadWorld != nil {
w, err := api.loadWorld(ctx, project, from, to)
return w, &cache.Status{}, err
}
var (
step timeseries.Duration
err error
)
cacheClients := map[db.ProjectId]constructor.Cache{}
var cacheStatus = &cache.Status{}
if !project.Multicluster() {
cacheClient := api.cache.GetCacheClient(project.Id)
if cacheStatus, err = cacheClient.GetStatus(); err != nil {
return nil, nil, err
}
cacheTo, err := cacheClient.GetTo()
if err != nil {
return nil, cacheStatus, err
}
if cacheTo.IsZero() || cacheTo.Before(from) {
return nil, cacheStatus, nil
}
if step, err = cacheClient.GetStep(from, to); err != nil {
return nil, cacheStatus, err
}
if cacheTo.Before(to) {
to = cacheTo
}
cacheClients[project.Id] = cacheClient
} else {
projects, err := api.db.GetProjects()
if err != nil {
return nil, nil, err
}
for _, mp := range project.Settings.MemberProjects {
p := projects[mp]
if p == nil {
klog.Warningln("member project not found:", mp)
return nil, nil, fmt.Errorf("member project not found: %s", mp)
}
cacheClient := api.cache.GetCacheClient(p.Id)
cs, err := cacheClient.GetStatus()
if err != nil {
return nil, nil, err
}
if cs.Error != "" {
cacheStatus.Error = cs.Error
}
if cs.LagMax > cacheStatus.LagMax {
cacheStatus.LagMax = cs.LagMax
}
if cs.LagAvg > cacheStatus.LagAvg {
cacheStatus.LagAvg = cs.LagAvg
}
cacheTo, err := cacheClient.GetTo()
if err != nil {
return nil, cacheStatus, err
}
if cacheTo.IsZero() || cacheTo.Before(from) {
return nil, cacheStatus, nil
}
st, err := cacheClient.GetStep(from, to)
if err != nil {
return nil, cacheStatus, err
}
if st > step {
step = st
}
if cacheTo.Before(to) {
to = cacheTo
}
cacheClients[p.Id] = cacheClient
}
}
step = increaseStepForBigDurations(from, to, step)
ctr := constructor.New(api.db, project, cacheClients, api.pricing)
world, err := ctr.LoadWorld(ctx, from, to, step, nil)
return world, cacheStatus, err
}
func (api *Api) LoadWorldByRequest(r *http.Request) (*model.World, *db.Project, *cache.Status, error) {
projectId := db.ProjectId(mux.Vars(r)["project"])
project, err := api.db.GetProject(projectId)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
klog.Warningln("project not found:", projectId)
return nil, nil, nil, nil
}
return nil, nil, nil, err
}
q := r.URL.Query()
from, to, _, truncated := api.getTimeContext(projectId, q.Get("from"), q.Get("to"), q.Get("incident"), q.Get("alert"))
world, cacheStatus, err := api.LoadWorld(r.Context(), project, from, to)
if world == nil {
step := increaseStepForBigDurations(from, to, 15*timeseries.Second)
world = model.NewWorld(from, to.Add(-step), step, step)
}
world.Ctx.Truncated = truncated
return world, project, cacheStatus, err
}
func (api *Api) getTimeContext(projectId db.ProjectId, fromStr, toStr, incidentKey, alertId string) (from timeseries.Time, to timeseries.Time, incident *model.ApplicationIncident, truncated bool) {
now := timeseries.Now()
defaultRange := api.cfg.DefaultTimeRange
if defaultRange <= 0 {
defaultRange = timeseries.Hour
}
from = utils.ParseTime(now, fromStr, now.Add(-defaultRange))
to = utils.ParseTime(now, toStr, now)
if from >= to {
from = to.Add(-defaultRange)
}
if incidentKey != "" {
var err error
if incident, err = api.db.GetIncidentByKey(projectId, incidentKey); err != nil {
klog.Warningln("failed to get incident:", err)
} else {
from = incident.OpenedAt.Add(-model.IncidentTimeOffset)
if incident.Resolved() {
to = incident.ResolvedAt.Add(model.IncidentTimeOffset)
} else {
to = now
}
if to.Sub(from) > MaxIncidentWindow {
from = to.Add(-MaxIncidentWindow)
truncated = true
}
}
}
if alertId != "" {
if a, err := api.db.GetAlert(projectId, alertId); err != nil {
klog.Warningln("failed to get alert:", err)
} else {
to = now
if a.ResolvedAt > 0 {
to = a.ResolvedAt
}
var keepFiringFor timeseries.Duration
if rule, err := api.db.GetAlertingRule(projectId, model.AlertingRuleId(a.RuleId)); err == nil && rule != nil {
keepFiringFor = rule.KeepFiringFor
}
from = to.Add(-timeseries.Hour - model.AlertTimeOffset - keepFiringFor)
if from.Before(a.OpenedAt.Add(-model.AlertTimeOffset)) {
from = a.OpenedAt.Add(-model.AlertTimeOffset)
}
}
}
return
}
func increaseStepForBigDurations(from, to timeseries.Time, step timeseries.Duration) timeseries.Duration {
duration := to.Sub(from)
switch {
case duration > 5*timeseries.Day:
return maxDuration(step, 60*timeseries.Minute)
case duration > timeseries.Day:
return maxDuration(step, 15*timeseries.Minute)
case duration > 12*timeseries.Hour:
return maxDuration(step, 10*timeseries.Minute)
case duration > 6*timeseries.Hour:
return maxDuration(step, 5*timeseries.Minute)
case duration > timeseries.Hour:
return maxDuration(step, timeseries.Minute)
}
return step
}
func maxDuration(d1, d2 timeseries.Duration) timeseries.Duration {
if d1 >= d2 {
return d1
}
return d2
}
func (api *Api) GetClickhouseClients(project *db.Project) clickhouse.Clients {
return clickhouse.GetClients(api.db, project, api.globalClickHouse)
}
func (api *Api) GetClickhouseClient(project *db.Project, memberProjectId string) (*clickhouse.Client, error) {
p := project
if project.Multicluster() {
if memberProjectId == "" {
return nil, errors.New("invalid member project id")
}
var err error
if p, err = api.db.GetProject(db.ProjectId(memberProjectId)); err != nil {
return nil, err
}
}
chs := api.GetClickhouseClients(p)
if chs.Error != nil {
return nil, chs.Error
}
if len(chs.Clients) == 0 {
return nil, nil
}
return chs.Clients[0], nil
}
func loadDbChangeEvents(ctx context.Context, api *Api, project *db.Project, app *model.Application, world *model.World) {
if !app.IsDatabase() {
return
}
ch, err := api.GetClickhouseClient(project, app.Id.ClusterId)
if err != nil {
klog.Warningln(err)
return
}
if ch == nil {
return
}
defer ch.Close()
targets := app.ListenTargets()
if len(targets) == 0 {
return
}
filters := []clickhouse.LogFilter{
{Name: "service.name", Op: "=", Value: "DatabaseChanges"},
}
for _, target := range targets {
filters = append(filters, clickhouse.LogFilter{Name: "db.target", Op: "=", Value: target})
}
q := clickhouse.LogQuery{
Ctx: timeseries.NewContext(world.Ctx.From, world.Ctx.To, 0),
Services: []string{"DatabaseChanges"},
Filters: filters,
Limit: 1000,
}
entries, err := ch.GetLogs(ctx, q)
if err != nil {
klog.Warningln(err)
return
}
logsQuery, _ := json.Marshal(map[string]any{ //nolint:errchkjson // only strings and plain structs, cannot fail
"view": "messages",
"filters": filters,
})
link := model.NewRouterLink("view", "overview").
SetParam("view", "logs").
SetArg("query", string(logsQuery))
app.SetDbChangeEvents(entries, world.Ctx.Step, link)
}
func GetApplicationId(r *http.Request) (model.ApplicationId, error) {
appIdStr, err := url.QueryUnescape(mux.Vars(r)["app"])
if err != nil {
return model.ApplicationId{}, err
}
appId, err := model.NewApplicationIdFromString(appIdStr, "")
if err != nil {
return model.ApplicationId{}, err
}
return appId, nil
}