Monitoring

Grafana API Call Flow 분석

토마스.dev 2019. 1. 17. 14:17

Grafana는 모니터링 대시보드로서 거의 반표준으로 사용되고 있다. 지원되는 기능이 많아 수정하는 경우가 별로 없지만, 필자의 경우 Grafana에서 지원하는 기본 Multi-Tenant 로는 부족하여 API를 직접 구현/추가하였다. 


이를 위해 먼저 Grafana API 호출 흐름을 이해하고 있어야 한다. 여기서는 Grafana Backend의 API Call 흐름을 간단하게 분석해보고자 한다.


Grafana의 Backend는 Golang으로 작성되어 있으며, Frontend는 AngularJS로 되어있다.

AngularJS는 Framework이라 구조에 대해 별로 평할게 없지만, Backend는 Golang을 모르더라도, 정말 쉽게 이해할수 있도록 가독성 높게 구현되어 있다.

Grafana Git Clone & Build

먼저 Grafana를 git clone하여 받는다.


$ git clone https://github.com/grafana/grafana.git


생성 기준 위치는 $GOPATH/src/github.com/grafana 이다.


안전한 버전으로 checkout를 한다.


$ git checkout v5.4.2


빌드를 먼저 해본다. 


$ make
go run build.go build
Version: 5.4.2, Linux Version: 5.4.2, Package Iteration: 1547514178
rm -r ./bin/linux-amd64/grafana-server
rm -r ./bin/linux-amd64/grafana-server.md5
go version
go version go1.11.1 linux/amd64
....
Done.
Done in 180.38s.


backend 만 빌드하려면 다음의 명령어를 사용한다.


$ make build-go


서버 실행 명령어는 다음과 같다


$ ./bin/linux-amd64/grafana-server -config=$(pwd)/conf/defaults.ini

API 흐름 분석

먼저 API 기본 흐름을 간단히 도식화 해보면,

API로 json data가 들어오면 이를 command(model) 에 담아 Handler 로 보내주게 된다. Handler는 이를 직접 처리하거나 각 Service로 이관하게 되는데, 이관 할때는 Bus를 통해 보내게 된다.


실제 구조 매핑이 어떻게 되는지 설명하자면,


API(Router)와 Handler는 api 디렉토리에 존재하며, Bus는 bus, Service는 services 디렉토리에 존재한다.

Command의 경우 models 디렉토리에 존재한다.


Bus에는 각 Service(alert, sql, datasource, dashboard 등)가 등록되어 있으며, command 타입에 따라 처리할 수 있는 Service가 호출된다.


이제부터 한가지 API를 가지고 자세히 설명하고자 한다. 


Grafana API중 Organization을 만드는 API가 있다.



이 API에 대한 Router는 pkg/api/api.go에 있다.


1
2
// create new org
apiRoute.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), Wrap(CreateOrg))


/api/orgs가 바로 API path이며 그 뒤에 나오는 3개는 Handler이다. quota의 경우 quota 체크, bind의 경우 command에 json data를 담아준다는 정도만 알고 넘어가자.


가장 마지막 CreateOrg가 바로 생성된 Command를 처리하는 Handler이다.


pkg/api/org.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// POST /api/orgs
func CreateOrg(c *m.ReqContext, cmd m.CreateOrgCommand) Response {
	if !c.IsSignedIn || (!setting.AllowUserOrgCreate && !c.IsGrafanaAdmin) {
		return Error(403, "Access denied", nil)
	}

	cmd.UserId = c.UserId
	if err := bus.Dispatch(&cmd); err != nil {
		if err == m.ErrOrgNameTaken {
			return Error(409, "Organization name taken", err)
		}
		return Error(500, "Failed to create organization", err)
	}

	metrics.M_Api_Org_Create.Inc()

	return JSON(200, &util.DynMap{
		"orgId":   cmd.Result.Id,
		"message": "Organization created",
	})
}

3~4 라인에서 간단히 인증/권한을 체크한 뒤에 실제 처리 로직이 나오는데, ReqContext의 경우 해당 API를 사용할 때의 환경정보가 기본적으로 셋팅된다. 7번 라인을 보면 User ID를 json data로 부터 받는게 아닌 API를 호출한 사용자로 셋팅하는 것을 알 수 있다.


CreateOrgCommand는 데이터를 담는 model이다.


pkg/models/org.go

1
2
3
4
5
6
7
type CreateOrgCommand struct {
	Name string `json:"name" binding:"Required"`

	// initial admin user for account
	UserId int64 `json:"-"`
	Result Org   `json:"-"`
}

UserId가 존재하지만 json 매핑이 없는 것을 알 수 있다. 그래서 해당 API에는 User id를 넣으려고 해도 할 수가 없는 것이다.


Result에는 Service에서 처리된 결과를 담는다.


pkg/models/org.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type Org struct {
	Id      int64
	Version int
	Name    string

	Address1 string
	Address2 string
	City     string
	ZipCode  string
	State    string
	Country  string

	Created time.Time
	Updated time.Time
}


bus.Dispatch로 Command를 보내게 되면 해당 Command를 처리할 수 있는 서비스의 Handler가 호출된다.


CreateOrgCommand를 처리하는 서비스는 pkg/services/sqlstore에 있다.


pkg/services/sqlstore/org.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
func CreateOrg(cmd *m.CreateOrgCommand) error {
	return inTransaction(func(sess *DBSession) error {

		if isNameTaken, err := isOrgNameTaken(cmd.Name, 0, sess); err != nil {
			return err
		} else if isNameTaken {
			return m.ErrOrgNameTaken
		}

		org := m.Org{
			Name:    cmd.Name,
			Created: time.Now(),
			Updated: time.Now(),
		}

		if _, err := sess.Insert(&org); err != nil {
			return err
		}

		user := m.OrgUser{
			OrgId:   org.Id,
			UserId:  cmd.UserId,
			Role:    m.ROLE_ADMIN,
			Created: time.Now(),
			Updated: time.Now(),
		}

		_, err := sess.Insert(&user)
		cmd.Result = org

		sess.publishAfterCommit(&events.OrgCreated{
			Timestamp: org.Created,
			Id:        org.Id,
			Name:      org.Name,
		})

		return err
	})
}

데이터 삽입이라 Transaction 처리를 하고 있으며, DB는 xorm으로 추상화 처리가 되어있다.


이렇게 해서 간단하게 Grafana API 흐름을 분석해 보았다. 이를 잘 이해하고 있으면 Custom API를 추가할때 손쉽게 할 수 있다.


필자의 경우 Prometheus multi-tenant가 필요했는데, query filter 기능을 넣고, 사용자의 권한에 따라 org 생성/삭제 및 Datasource setting 기능을 API로 추가하였다.