grafana


grafana install

介绍下grafana+influxdb环境搭建。数据采用上篇介绍的采集cpu的简单demo在grafana上配置及显示。

grafana 官网

参考文档grafana doc

ubuntu安装并启动操作:

1
2
3
4
5
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.2_amd64.deb
sudo apt-get install -y adduser libfontconfig1
sudo dpkg -i grafana_5.1.2_amd64.deb

systemctl start grafana-server

启动成功后,grafana-server 监听3000端口。默认用户名和密码: admin@admin


grafana配置

1 首先需要添加添加数据源。点击Add data source

2 选择左侧dock栏上方的+图标创建一个dashbord,配置如下:

说明:

  • Name: 数据源设置名称,见名知意即可。
  • Type: 选择influxDB。grafana可支持多种后端数据源,其它类型在此不表。

  • URL: influxDB的服务地址。
  • Access 方式: 选择Server方式即可,这样可以避免与influxDB CORS访问问题。

  • Dababase: influxDB中的数据库名称
  • Min Time Interval: 最小时间间隔。根据实际采样数据,选择合适的最小时间间隔。这个值会影响到后面的influx 查询表达式中group by数值。

3 添加一个panel,选择graph类型

4 编辑panel

说明: grafana针对influxDB查询语句加载了与之匹配的配置,方便通过页面生成合适的查询语句。 influxDB支持的丰富内置函数在select项里可以一览无余。比如这里使用mean求平均,或许使用median更好一些。 group by中的time可设的最小值即为前面配置数据源时候的Min Time Interval. fill()表示如果采样的数据在influxDB中如果没有数值的时候进行填充。null表示如果没有就不要这部分数值。 添加过程中可以通过 Query Inspector中的表达式和结果进行调试。

5 grafana支持鼠标拖拽,调整大小。调整好后这些参数都会保存到数据库中,保持个性化和持久化。

实战

1
以下编写一个简单的exporter, 然后加入到fasthttp入口处,统计所有的API 请求数量。
package exporter
import (
	"encoding/json"
	"sync"
	"sync/atomic"

	"context"
	"errors"
	"fmt"
	"net"
	"os"
	"strings"
	"time"

	"github.com/coreos/etcd/clientv3"
	"github.com/qiangxue/fasthttp-routing"
)

const (
	defaultHoldLifeTime = 65 * time.Second
)

var cc = newCollector()

func init() {
	go RegistMySelf(30)
}

func allocInt64() *int64 {
	i := new(int64)
	*i = 1
	return i
}

type collector struct {
	autoClear *time.Timer
	data      sync.Map
}

func newCollector() *collector {
	inst := &collector{
		data: sync.Map{},
	}

	inst.autoClear = time.AfterFunc(defaultHoldLifeTime, func() {
		inst.Clear()
	})

	return inst
}

func (c *collector) Incr(path []byte) {
	p := string(path)

	v, ok := c.data.Load(p)
	if ok {
		atomic.AddInt64(v.(*int64), 1)
	} else {
		// FIXME: bug ???
		c.data.Store(p, allocInt64())
	}
}

func (c *collector) Clear() {
	c.data.Range(func(k, v interface{}) bool {
		atomic.StoreInt64(v.(*int64), 0)
		return true
	})
}

func (c *collector) Stat() []byte {
	result := make(map[string]int64)

	oneByOne := func(k, v interface{}) bool {
		result[k.(string)] = atomic.SwapInt64(v.(*int64), 0)
		return true
	}

	c.data.Range(oneByOne)

	c.autoClear.Reset(defaultHoldLifeTime)

	resultJson, _ := json.Marshal(result)

	return resultJson
}

func ExportCountHandler(ctx *routing.Context) error {

	cc.Incr(ctx.Request.URI().Path())

	return nil
}

func ExportStatHandler(ctx *routing.Context) error {
	ctx.SetContentType("application/json")

	ctx.Write(cc.Stat())

	return nil
}

func getLocalAddr(port uint16) (string, error) {
	conn, err := net.Dial("udp", "www.sogou.com:80")
	if err != nil {
	    return "", err
	}
	defer conn.Close()

	host, _, err := net.SplitHostPort(conn.LocalAddr().String())
	if err != nil {
	    return "", err
	}

	return net.JoinHostPort(host, fmt.Sprintf("%d", port))
}

func MySelf() string {
	hostName, _ := os.Hostname()
	return `/xxx_service/` + hostName
}

func RegistMySelf(ttl int64) {
	cli, err := clientv3.New(clientv3.Config{
		Endpoints:   strings.Split(util.AppConfig.EtcdCluster, ","),
		DialTimeout: 3 * time.Second,
	})

	if err != nil {
        fmt.Errorf(os.Stderr, "New ectd client error: %s", err.Error())
		return
	}

	_myself := MySelf()

	resp := NewLeaseGrant(cli, _myself, ttl+10)
	if resp == nil {
        fmt.Errorf(os.Stderr, "Invalid lease instance")
		return
	}

	stop := make(chan struct{})

	util.RegistExitHook(func() error {
		stop <- struct{}{}
		if cli == nil {
			return nil
		}

		defer cli.Close()

		if delResp, err := cli.Delete(context.TODO(), _myself); err == nil {
			fmt.Println("sai yo na ra:", delResp)
		}

		return err
	})

	t := time.NewTicker(time.Duration(ttl) * time.Second)

	for {
		select {
		case <-t.C:
			kres, e := cli.KeepAliveOnce(context.TODO(), resp.ID)
			if e != nil {
				resp = NewLeaseGrant(cli, _myself, 2*ttl)
				if resp == nil {
					fmt.Errorf(os.Stderr, "Invalid etcd lease grant")
				}
			} else {
				fmt.Println("[D] keepalive response, version:", kres.Revision, "raft_term:", kres.RaftTerm)
			}
		case <-stop:
			t.Stop()
			return
		}
	}

	return
}

func NewLeaseGrant(client *clientv3.Client, value string, ttl int64) *clientv3.LeaseGrantResponse {
	if client == nil {
		return nil
	}

	resp, err := client.Grant(context.TODO(), ttl*2) // must longer
	if err != nil {
		fmt.Errorf(os.Stderr, "Grant error: %s", err.Error())
		return nil
	}

	serviceAddr := getLocalAddr()

	_, err = client.Put(context.TODO(), value, serviceAddr, clientv3.WithLease(resp.ID))
	if err != nil {
		fmt.Errorf(os.Stderr, "Put etcd error: %s", err.Error())
		return nil
	}

	return resp
}
  

配置dashboard时,尽可能的不要在metric中硬编码。可将一些参数放到Variables中。