Skip to content

Commit

Permalink
feature(api): add swagger specification generation (#891)
Browse files Browse the repository at this point in the history
  • Loading branch information
almostinf authored Aug 17, 2023
1 parent 0dc273e commit 31167c6
Show file tree
Hide file tree
Showing 35 changed files with 1,000 additions and 142 deletions.
3 changes: 3 additions & 0 deletions Dockerfile.api
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ FROM golang:1.18 as builder
COPY go.mod go.sum /go/src/github.com/moira-alert/moira/
WORKDIR /go/src/github.com/moira-alert/moira
RUN go mod download
RUN go install github.com/swaggo/swag/cmd/[email protected]

COPY . /go/src/github.com/moira-alert/moira/

RUN /go/bin/swag init -g api/handler/handler.go

ARG GO_VERSION="GoVersion"
ARG GIT_COMMIT="git_Commit"
ARG MoiraVersion="MoiraVersion"
Expand Down
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ lint:
mock:
. ./generate_mocks.sh

.PHONY: install-swag
install-swag:
go install github.com/swaggo/swag/cmd/[email protected]

.PHONY: spec
spec:
echo "Generating Swagger documentation"
swag init -g api/handler/handler.go
swag fmt

.PHONY: validate-spec
validate-spec:
openapi-generator-cli validate -i docs/swagger.yaml

.PHONY: test
test:
echo 'mode: atomic' > coverage.txt && go list ./... | xargs -n1 -I{} sh -c 'go test -race -v -bench=. -covermode=atomic -coverprofile=coverage.tmp {} && tail -n +2 coverage.tmp >> coverage.txt' && rm coverage.tmp
Expand Down
8 changes: 4 additions & 4 deletions api/dto/contact.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ func (*ContactList) Render(w http.ResponseWriter, r *http.Request) error {
}

type Contact struct {
Type string `json:"type"`
Value string `json:"value"`
ID string `json:"id,omitempty"`
User string `json:"user,omitempty"`
Type string `json:"type" example:"mail"`
Value string `json:"value" example:"[email protected]"`
ID string `json:"id,omitempty" example:"1dd38765-c5be-418d-81fa-7a5f879c2315"`
User string `json:"user,omitempty" example:""`
TeamID string `json:"team_id,omitempty"`
}

Expand Down
6 changes: 3 additions & 3 deletions api/dto/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
)

type EventsList struct {
Page int64 `json:"page"`
Size int64 `json:"size"`
Total int64 `json:"total"`
Page int64 `json:"page" example:"0"`
Size int64 `json:"size" example:"100"`
Total int64 `json:"total" example:"10"`
List []moira.NotificationEvent `json:"list"`
}

Expand Down
4 changes: 2 additions & 2 deletions api/dto/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ const (
)

type NotifierState struct {
State string `json:"state"`
Message string `json:"message,omitempty"`
State string `json:"state" example:"ERROR"`
Message string `json:"message,omitempty" example:"Moira has been turned off for maintenance"`
}

func (*NotifierState) Render(w http.ResponseWriter, r *http.Request) error {
Expand Down
4 changes: 2 additions & 2 deletions api/dto/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

type NotificationsList struct {
Total int64 `json:"total"`
Total int64 `json:"total" example:"0"`
List []*moira.ScheduledNotification `json:"list"`
}

Expand All @@ -17,7 +17,7 @@ func (*NotificationsList) Render(w http.ResponseWriter, r *http.Request) error {
}

type NotificationDeleteResponse struct {
Result int64 `json:"result"`
Result int64 `json:"result" example:"0"`
}

func (*NotificationDeleteResponse) Render(w http.ResponseWriter, r *http.Request) error {
Expand Down
4 changes: 2 additions & 2 deletions api/dto/pattern.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func (*PatternList) Render(w http.ResponseWriter, r *http.Request) error {
}

type PatternData struct {
Metrics []string `json:"metrics"`
Pattern string `json:"pattern"`
Metrics []string `json:"metrics" example:"DevOps.my_server.hdd.freespace_mbytes, DevOps.my_server.hdd.freespace_mbytes, DevOps.my_server.db.*"`
Pattern string `json:"pattern" example:"Devops.my_server.*"`
Triggers []TriggerModel `json:"triggers"`
}
8 changes: 4 additions & 4 deletions api/dto/tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import (
)

type TagsData struct {
TagNames []string `json:"list"`
TagNames []string `json:"list" example:"cpu"`
}

func (*TagsData) Render(w http.ResponseWriter, r *http.Request) error {
return nil
}

type MessageResponse struct {
Message string `json:"message"`
Message string `json:"message" example:"tag deleted"`
}

func (*MessageResponse) Render(w http.ResponseWriter, r *http.Request) error {
Expand All @@ -28,8 +28,8 @@ type TagsStatistics struct {
}

type TagStatistics struct {
TagName string `json:"name"`
Triggers []string `json:"triggers"`
TagName string `json:"name" example:"cpu"`
Triggers []string `json:"triggers" example:"bcba82f5-48cf-44c0-b7d6-e1d32c64a88c"`
Subscriptions []moira.SubscriptionData `json:"subscriptions"`
}

Expand Down
10 changes: 5 additions & 5 deletions api/dto/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@ var (
)

type ProblemOfTarget struct {
Argument string `json:"argument"`
Type typeOfProblem `json:"type,omitempty"`
Description string `json:"description,omitempty"`
Position int `json:"position"`
Argument string `json:"argument" example:"consolidateBy"`
Type typeOfProblem `json:"type,omitempty" example:"warn"`
Description string `json:"description,omitempty" example:"This function affects only visual graph representation. It is meaningless in Moira"`
Position int `json:"position" example:"0"`
Problems []ProblemOfTarget `json:"problems,omitempty"`
}

Expand All @@ -123,7 +123,7 @@ func (p *ProblemOfTarget) hasError() bool {
}

type TreeOfProblems struct {
SyntaxOk bool `json:"syntax_ok"`
SyntaxOk bool `json:"syntax_ok" example:"true"`
TreeOfProblems *ProblemOfTarget `json:"tree_of_problems,omitempty"`
}

Expand Down
12 changes: 6 additions & 6 deletions api/dto/team.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ const (

// TeamModel is a structure that represents team entity in HTTP transfer
type TeamModel struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
ID string `json:"id" example:"d5d98eb3-ee18-4f75-9364-244f67e23b54"`
Name string `json:"name" example:"Infrastructure Team"`
Description string `json:"description" example:"Team that holds all members of infrastructure division"`
}

// NewTeamModel is a constructor function that creates a new TeamModel using moira.Team
Expand Down Expand Up @@ -59,7 +59,7 @@ func (t TeamModel) ToMoiraTeam() moira.Team {

// SaveTeamResponse is a structure to return team creation result in HTTP response
type SaveTeamResponse struct {
ID string `json:"id"`
ID string `json:"id" example:"d5d98eb3-ee18-4f75-9364-244f67e23b54"`
}

// Render is a function that implements chi Renderer interface for SaveTeamResponse
Expand All @@ -79,7 +79,7 @@ func (UserTeams) Render(w http.ResponseWriter, r *http.Request) error {

// TeamMembers is a structure that represents a team members in HTTP transfer
type TeamMembers struct {
Usernames []string `json:"usernames"`
Usernames []string `json:"usernames" example:"anonymous"`
}

// Bind is a method that implements Binder interface from chi and checks that validity of data in request
Expand All @@ -96,7 +96,7 @@ func (TeamMembers) Render(w http.ResponseWriter, r *http.Request) error {
}

type TeamSettings struct {
TeamID string `json:"team_id"`
TeamID string `json:"team_id" example:"d5d98eb3-ee18-4f75-9364-244f67e23b54"`
Contacts []moira.ContactData `json:"contacts"`
Subscriptions []moira.SubscriptionData `json:"subscriptions"`
}
Expand Down
44 changes: 22 additions & 22 deletions api/dto/triggers.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,43 +36,43 @@ func (*TriggersList) Render(http.ResponseWriter, *http.Request) error {

type Trigger struct {
TriggerModel
Throttling int64 `json:"throttling"`
Throttling int64 `json:"throttling" example:"0"`
}

// TriggerModel is moira.Trigger api representation
type TriggerModel struct {
// Trigger unique ID
ID string `json:"id"`
ID string `json:"id" example:"292516ed-4924-4154-a62c-ebe312431fce"`
// Trigger name
Name string `json:"name"`
Name string `json:"name" example:"Not enough disk space left"`
// Description string
Desc *string `json:"desc,omitempty"`
Desc *string `json:"desc,omitempty" example:"check the size of /var/log"`
// Graphite-like targets: t1, t2, ...
Targets []string `json:"targets"`
Targets []string `json:"targets" example:"devOps.my_server.hdd.freespace_mbytes"`
// WARN threshold
WarnValue *float64 `json:"warn_value"`
WarnValue *float64 `json:"warn_value" example:"500"`
// ERROR threshold
ErrorValue *float64 `json:"error_value"`
ErrorValue *float64 `json:"error_value" example:"1000"`
// Could be: rising, falling, expression
TriggerType string `json:"trigger_type"`
TriggerType string `json:"trigger_type" example:"rising"`
// Set of tags to manipulate subscriptions
Tags []string `json:"tags"`
Tags []string `json:"tags" example:"server,disk"`
// When there are no metrics for trigger, Moira will switch metric to TTLState state after TTL seconds
TTLState *moira.TTLState `json:"ttl_state,omitempty"`
TTLState *moira.TTLState `json:"ttl_state,omitempty" example:"NODATA"`
// When there are no metrics for trigger, Moira will switch metric to TTLState state after TTL seconds
TTL int64 `json:"ttl,omitempty"`
TTL int64 `json:"ttl,omitempty" example:"600"`
// Determines when Moira should monitor trigger
Schedule *moira.ScheduleData `json:"sched,omitempty"`
// Used if you need more complex logic than provided by WARN/ERROR values
Expression string `json:"expression"`
Expression string `json:"expression" example:""`
// Graphite patterns for trigger
Patterns []string `json:"patterns"`
Patterns []string `json:"patterns" example:""`
// Shows if trigger is remote (graphite-backend) based or stored inside Moira-Redis DB
IsRemote bool `json:"is_remote"`
IsRemote bool `json:"is_remote" example:"false"`
// If true, first event NODATA → OK will be omitted
MuteNewMetrics bool `json:"mute_new_metrics"`
MuteNewMetrics bool `json:"mute_new_metrics" example:"false"`
// A list of targets that have only alone metrics
AloneMetrics map[string]bool `json:"alone_metrics"`
AloneMetrics map[string]bool `json:"alone_metrics" example:"t1:true"`
// Datetime when the trigger was created
CreatedAt *time.Time `json:"created_at"`
// Datetime when the trigger was updated
Expand Down Expand Up @@ -366,7 +366,7 @@ type TriggerCheckResponse struct {

type TriggerCheck struct {
*moira.CheckData
TriggerID string `json:"trigger_id"`
TriggerID string `json:"trigger_id" example:"trigger_id"`
}

func (*TriggerCheck) Render(http.ResponseWriter, *http.Request) error {
Expand All @@ -380,7 +380,7 @@ func (*MetricsMaintenance) Bind(*http.Request) error {
}

type TriggerMaintenance struct {
Trigger *int64 `json:"trigger"`
Trigger *int64 `json:"trigger" example:"1594225165"`
Metrics map[string]int64 `json:"metrics"`
}

Expand All @@ -389,16 +389,16 @@ func (*TriggerMaintenance) Bind(*http.Request) error {
}

type ThrottlingResponse struct {
Throttling int64 `json:"throttling"`
Throttling int64 `json:"throttling" example:"0"`
}

func (*ThrottlingResponse) Render(http.ResponseWriter, *http.Request) error {
return nil
}

type SaveTriggerResponse struct {
ID string `json:"id"`
Message string `json:"message"`
ID string `json:"id" example:"trigger_id"`
Message string `json:"message" example:"trigger created"`
CheckResult TriggerCheckResponse `json:"checkResult,omitempty"`
}

Expand Down Expand Up @@ -426,7 +426,7 @@ type TriggerDump struct {
}

type TriggersSearchResultDeleteResponse struct {
PagerID string `json:"pager_id"`
PagerID string `json:"pager_id" example:"292516ed-4924-4154-a62c-ebe312431fce"`
}

func (TriggersSearchResultDeleteResponse) Render(http.ResponseWriter, *http.Request) error {
Expand Down
2 changes: 1 addition & 1 deletion api/dto/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func (*UserSettings) Render(w http.ResponseWriter, r *http.Request) error {
}

type User struct {
Login string `json:"login"`
Login string `json:"login" example:"john"`
}

func (*User) Render(w http.ResponseWriter, r *http.Request) error {
Expand Down
32 changes: 32 additions & 0 deletions api/error_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,35 @@ var ErrNotFound = &ErrorResponse{HTTPStatusCode: http.StatusNotFound, StatusText

// ErrMethodNotAllowed is default 405 router method not allowed
var ErrMethodNotAllowed = &ErrorResponse{HTTPStatusCode: http.StatusMethodNotAllowed, StatusText: "Method not allowed."}

// Examples for `swaggo`:

type ErrorInternalServerExample struct {
StatusText string `json:"status" example:"Internal Server Error"`
ErrorText string `json:"error" example:"server error during request handling"`
}

type ErrorInvalidRequestExample struct {
StatusText string `json:"status" example:"Invalid request"`
ErrorText string `json:"error" example:"resource with the ID does not exist"`
}

type ErrorRenderExample struct {
StatusText string `json:"status" example:"Error rendering response"`
ErrorText string `json:"error" example:"rendering error"`
}

type ErrorNotFoundExample struct {
StatusText string `json:"status" example:"Resource not found"`
ErrorText string `json:"error" example:"resource with ID '66741a8c-c2ba-4357-a2c9-ee78e0e7' does not exist"`
}

type ErrorForbiddenExample struct {
StatusText string `json:"status" example:"Forbidden"`
ErrorText string `json:"error" example:"you cannot access this resource"`
}

type ErrorRemoteServerUnavailableExample struct {
StatusText string `json:"status" example:"Remote server unavailable"`
ErrorText string `json:"error" example:"Remote server error, please contact administrator"`
}
18 changes: 18 additions & 0 deletions api/handler/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@ package handler

import "net/http"

type ContactExample struct {
Type string `json:"type" example:"telegram"`
Label string `json:"label" example:"Telegram"`
}

type ConfigurationResponse struct {
RemoteAllowed bool `json:"remoteAllowed" example:"false"`
Contacts []ContactExample `json:"contacts"`
}

// nolint: gofmt,goimports
//
// @summary Get available configuration
// @id get-web-config
// @tags config
// @produce json
// @success 200 {object} ConfigurationResponse "Configuration fetched successfully"
// @router /config [get]
func getWebConfig(configContent []byte) http.HandlerFunc {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Type", "application/json")
Expand Down
Loading

0 comments on commit 31167c6

Please sign in to comment.