golang接入skywalking

环境

Go 1.14.7
GoLand 2020.1

升级到 Go 1.17.7,因为 golang.org/x/net 包要求 go 1.17
如果升级到 Go 1.17.7,那么GoLand将加载不到 Go SDK,因此 GoLand 要升级

导入包

go get -u github.com/SkyAPM/go2sky 
go get -u github.com/SkyAPM/go2sky-plugins/gin/v3

以Gin框架做个演示

package main

import (
	"fmt"
	"net/http"
	"time"

	"github.com/SkyAPM/go2sky"
	v3 "github.com/SkyAPM/go2sky-plugins/gin/v3"
	"github.com/SkyAPM/go2sky/reporter"
	"github.com/gin-gonic/gin"
)

func main() {
    
    
	r := gin.New()

	// skywalking服务地址 192.168.2.44:11800
	rp, err := reporter.NewGRPCReporter("192.168.2.44:11800", reporter.WithCheckInterval(time.Second))
	if err != nil {
    
    
		fmt.Println("create gosky reporter failed!")
	}
	defer rp.Close()

	// 服务名 test-demo
	tracer, err := go2sky.NewTracer("test-demo", go2sky.WithReporter(rp))

	r.Use(v3.Middleware(r, tracer))

	r.GET("/test0", test0)
	r.GET("/test1", test1)
	r.GET("/test2", test2)

	r.Run()
}

func test0(c *gin.Context) {
    
    
	result := make(map[string]interface{
    
    })
	result["code"] = 0
	result["msg"] = ""
	result["data"] = "test0"
	c.JSON(http.StatusOK, result)
}

func test1(c *gin.Context) {
    
    
	result := make(map[string]interface{
    
    })
	result["code"] = 0
	result["msg"] = ""
	result["data"] = "test1"
	c.JSON(http.StatusOK, result)
}

func test2(c *gin.Context) {
    
    
	result := make(map[string]interface{
    
    })
	result["code"] = 0
	result["msg"] = ""
	result["data"] = "test2"
	c.JSON(http.StatusOK, result)
}

启动后访问这几个接口。

服务名是上面定义的 test-demo

查看 v3.Middleware源码

// 组件名称,5006 代表 Gin
const componentIDGINHttpServer = 5006

//Middleware gin middleware return HandlerFunc  with tracing.
func Middleware(engine *gin.Engine, tracer *go2sky.Tracer) gin.HandlerFunc {
    
    
	if engine == nil || tracer == nil {
    
    
		return func(c *gin.Context) {
    
    
			c.Next()
		}
	}

	return func(c *gin.Context) {
    
    
        // 创建 EntrySpan,并且将 c.Request.Header 中的信息注入到 c.Request.Context() 中去,得到新的 ctx
        // 此时 c.Request.Context() 并没有改变,只使用了它的拷贝
        // 返回的这个 ctx 很重要,是建立联系的关键
		span, ctx, err := tracer.CreateEntrySpan(c.Request.Context(), getOperationName(c), func(key string) (string, error) {
    
    
			return c.Request.Header.Get(key), nil
		})
		if err != nil {
    
    
			c.Next()
			return
		}
         // 设置 Tag 中的 组件 信息,此处就是 Gin
		span.SetComponent(componentIDGINHttpServer)
         // 设置 Tag 中的 http.method 信息
		span.Tag(go2sky.TagHTTPMethod, c.Request.Method)
         // 设置 Tag 中的 url 信息
		span.Tag(go2sky.TagURL, c.Request.Host+c.Request.URL.Path)
         // 设置 Span 样式
		span.SetSpanLayer(agentv3.SpanLayer_Http)
		// 将 ctx 覆盖掉 c.Request.Context(),便于随着 c 一起传递,也就是说之后 c.Request.Context() 得到
         // 的 ctx 既包括了 c 原先的 context 内容,也包括了来自下游服务的内容
		c.Request = c.Request.WithContext(ctx)

		c.Next()

		if len(c.Errors) > 0 {
    
    
			span.Error(time.Now(), c.Errors.String())
		}
         // 设置 Tag 中的 status_code
		span.Tag(go2sky.TagStatusCode, strconv.Itoa(c.Writer.Status()))
         // 结束 Span,记录结束时间,从创建Span到结束Span之间的时间就是此跨度的时间
		span.End()
	}
}

// 将请求方式和完整地址拼在一起作为端点
func getOperationName(c *gin.Context) string {
    
    
	return fmt.Sprintf("/%s%s", c.Request.Method, c.FullPath())
}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

下面做一个多跨度的演示

client --> server1 --> server2 --> server3

server1

func main() {
    
    
	r := gin.New()
	rp, err := reporter.NewGRPCReporter("192.168.2.44:11800", reporter.WithCheckInterval(time.Second))
	if err != nil {
    
    
		fmt.Println("create gosky reporter failed!")
         return
	}
	defer rp.Close()
	tracer, err := go2sky.NewTracer("test-demo1", go2sky.WithReporter(rp))
	r.Use(v3.Middleware(r, tracer))
	r.GET("/test", test)
	r.Run(":7001")
}

func test(c *gin.Context) {
    
    
	util.Get("http://127.0.0.1:7002/test")

	result := make(map[string]interface{
    
    })
	result["code"] = 0
	result["msg"] = ""
	result["data"] = "test"
	c.JSON(http.StatusOK, result)
}

server2

func main() {
    
    
	r := gin.New()
	rp, err := reporter.NewGRPCReporter("192.168.2.44:11800", reporter.WithCheckInterval(time.Second))
	if err != nil {
    
    
		fmt.Println("create gosky reporter failed!")
        return
	}
	defer rp.Close()
	tracer, err := go2sky.NewTracer("test-demo2", go2sky.WithReporter(rp))
	r.Use(v3.Middleware(r, tracer))
	r.GET("/test", test)
	r.Run(":7002")
}

func test(c *gin.Context) {
    
    
	util.Get("http://127.0.0.1:7003/test")

	result := make(map[string]interface{
    
    })
	result["code"] = 0
	result["msg"] = ""
	result["data"] = "test"
	c.JSON(http.StatusOK, result)
}

server3

func main() {
    
    
	r := gin.New()
	rp, err := reporter.NewGRPCReporter("192.168.2.44:11800", reporter.WithCheckInterval(time.Second))
	if err != nil {
    
    
		fmt.Println("create gosky reporter failed!")
        return
	}
	defer rp.Close()
	tracer, err := go2sky.NewTracer("test-demo3", go2sky.WithReporter(rp))
	r.Use(v3.Middleware(r, tracer))
	r.GET("/test", test)
	r.Run(":7003")
}

func test(c *gin.Context) {
    
    
	result := make(map[string]interface{
    
    })
	result["code"] = 0
	result["msg"] = ""
	result["data"] = "test"
	c.JSON(http.StatusOK, result)
}

util.Get

func Get(link string) (response string, err error) {
    
    
	client := http.Client{
    
    Timeout: time.Second * 10}
	resp, err := client.Get(link)
	if err != nil {
    
    
		return response, err
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	return string(body), nil
}

访问地址:http://127.0.0.1:7001/test

查看追踪,发现这三个服务没有连接起来。

在这里插入图片描述

原因就是github.com/SkyAPM/go2sky这个包做不到像 javaagent那样无侵入智能上报,我们需要通过上下文来自己建立连接,埋点。

首先先要理解什么是上下游,这个有点反常识,让我调试了很久!!!

client --> server1 --> server2
           下游服务     上游服务

Span有三种类型:LocalSpan、EntrySpan、ExitSpan

LocalSpan:可以用来表示本程序内的一次调用。
EntrySpan:用来从下游服务提取context信息。
ExitSpan: 用来向上游服务注入context信息。

参数解释

peer 上游服务的Host,会在Tag信息中展示
operationName 就是调用名称(端点),意思要明确

于是我放弃了github.com/SkyAPM/go2sky-plugins/gin/v3包,自己封装中间件也很简单。然后修改Get方法

func Get(tracer *go2sky.Tracer, link string) (response string, err error) {
    
    
	client := http.Client{
    
    Timeout: time.Second * 10}
	var reqest *http.Request
	reqest, err = http.NewRequest("GET", link, nil)
	if err != nil {
    
    
		return
	}

	url := reqest.URL
	operationName := url.Scheme + "://" + url.Host + url.Path
	span, err := tracer.CreateExitSpan(reqest.Context(), operationName, url.Host, func(key, value string) error {
    
    
		reqest.Header.Set(key, value)
		return nil
	})
	if err != nil {
    
    
		return
	}
	span.SetComponent(componentIDGINHttpServer)
	span.Tag(go2sky.TagHTTPMethod, reqest.Method)
	span.Tag(go2sky.TagURL, link)
	span.SetSpanLayer(agentv3.SpanLayer_Http)
	defer span.End()

	resp, err := client.Do(reqest)
	if err != nil {
    
    
		return response, err
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	return string(body), nil
}

效果为

在这里插入图片描述

拓扑图是对的,一个请求产生了5个记录,但是为什么没有串连起来呢?就是一个 TraceID 串联5个跨度的那种。
在这里插入图片描述

最后发现,要想将一个EntrySpan和一个ExitSpan连起来,那么tracer.CreateExitSpan()的第一个参数ctxAtracer.CreateEntrySpan()的返回值中的ctxB,意思是将EntrySpanctx注入到ExitSpan中去构成关联。于是上面的Get方法又要修改

func Get(tracer *go2sky.Tracer, ctx context.Context, link string) (response string, err error) {
    
    
	client := http.Client{
    
    Timeout: time.Second * 10}
	var reqest *http.Request
	reqest, err = http.NewRequest("GET", link, nil)
	if err != nil {
    
    
		return
	}

	url := reqest.URL
	operationName := url.Scheme + "://" + url.Host + url.Path
	span, err := tracer.CreateExitSpan(ctx, operationName, url.Host, func(key, value string) error {
    
    
		reqest.Header.Set(key, value)
		return nil
	})
	if err != nil {
    
    
		return
	}
	span.SetComponent(componentIDGINHttpServer)
	span.Tag(go2sky.TagHTTPMethod, reqest.Method)
	span.Tag(go2sky.TagURL, link)
	span.SetSpanLayer(agentv3.SpanLayer_Http)

	resp, err := client.Do(reqest)
	if err != nil {
    
    
		return response, err
	}

	span.End()

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	return string(body), nil
}

调整之后的结果。
在这里插入图片描述

在这里插入图片描述

这次就对了。

其他代码

func Middleware(tracer *go2sky.Tracer) gin.HandlerFunc {
    
    
	if tracer == nil {
    
    
		return func(c *gin.Context) {
    
    
			c.Next()
		}
	}

	return func(c *gin.Context) {
    
    
		operationName := c.FullPath()
		span, ctx, err := tracer.CreateEntrySpan(c.Request.Context(), operationName, func(key string) (string, error) {
    
    
			return c.Request.Header.Get(key), nil
		})
		if err != nil {
    
    
			c.Next()
			return
		}
		span.SetComponent(componentIDGINHttpServer)
		span.Tag(go2sky.TagHTTPMethod, c.Request.Method)
		span.Tag(go2sky.TagURL, c.Request.Host+c.Request.URL.Path)
		span.SetSpanLayer(agentv3.SpanLayer_Http)

		c.Request = c.Request.WithContext(ctx)

		c.Next()

		if len(c.Errors) > 0 {
    
    
			span.Error(time.Now(), c.Errors.String())
		}
		span.Tag(go2sky.TagStatusCode, strconv.Itoa(c.Writer.Status()))
		span.End()
	}
}

server1

var tr *go2sky.Tracer

func main() {
    
    
	r := gin.New()
	rp, err := reporter.NewGRPCReporter("192.168.2.44:11800", reporter.WithCheckInterval(time.Second))
	if err != nil {
    
    
		fmt.Println("create gosky reporter failed!")
		return
	}
	defer rp.Close()

	tr, err = go2sky.NewTracer("test-demo1", go2sky.WithReporter(rp))
	r.Use(util.Middleware(tr))
	r.GET("/test", test)
	r.Run(":7001")
}

func test(c *gin.Context) {
    
    
	util.Get(tr, c.Request.Context(), "http://127.0.0.1:7002/test")

	result := make(map[string]interface{
    
    })
	result["code"] = 0
	result["msg"] = ""
	result["data"] = "test"
	c.JSON(http.StatusOK, result)
}

测试一下日志功能

增加代码

span.Log(time.Now(), "test log info")
span.Error(time.Now(), "test log error")

在这里插入图片描述

有 error 的地方会被标红

在这里插入图片描述

在实际应用中,我们不可能将参数ctx到处去传参,这会破坏整个项目,所以这个ctx需要做到全局缓存,那么存储的key是什么呢?在Gin中,一个请求过来是由一个Goroutine来处理的,我们可以将GoroutineID作为key,并使用并发安全的sync.Map。使用这种方法的时候你需要明确每个协程的边界。

func goID() uint64 {
    
    
	b := make([]byte, 64)
	b = b[:runtime.Stack(b, false)]
	b = bytes.TrimPrefix(b, []byte("goroutine "))
	b = b[:bytes.IndexByte(b, ' ')]
	n, _ := strconv.ParseUint(string(b), 10, 64)
	return n
}

再次经过一番整理和优化。

完整代码:https://github.com/phprao/go-skywalking

最后不得不说,这个go2sky需要手动埋点,严重侵入代码,要想接入 rpc, mysql, redis 还需要各种改动,不太适合工程化,并不像java语言支持的那么好,简直是零侵入。而PHP语言的话,可以安装 skywalking 扩展,也是自动探针,无侵入的,可以Hook绝大部分常用调用。

猜你喜欢

转载自blog.csdn.net/raoxiaoya/article/details/123248032