【链路追踪】「Go语言」OpenTelemetry实现[gin, gRPC, log, gorm, redis]的集成

news2024/10/5 12:53:54

文章目录

  • 一、OpenTelemetry的前世今生
    • OpenTracing
    • OpenCensus
    • 大一统
  • 二、OpenTelemetry快速体验
    • go快速体验
    • OpenTelemetry系统架构
    • 尾部采样
  • 三、通过http完成span传输
    • 函数中传递span的context
  • 四、自定义inject和extract源码
  • 五、gRPC集成
  • 自用框架集成(无视即可)
  • 六、log集成
  • 七、gorm集成
  • 八、gin集成
  • 九、redis集成


一、OpenTelemetry的前世今生

OpenTelemetry中文文档:https://github.com/open-telemetry/docs-cn/blob/main/OT.md

之前用的是jaeger实现链路追踪,但是想要换成Zipkin等框架或集成指标监控或集成日志会换框架很麻烦。

OpenTracing

OpenTracing制定了一套平台无关、厂商无关的协议标准,使得开发人员能够方便的添加或更换底层APM的实现。

在2016年11月的时候发生了一件里程碑事件,CNCF.io接受OpenTracing,同时这也是CNCF的第三个项目,前两个都已经鼎鼎大名了:Kubernetes和Prometheus,由此可见开源世界对APM的重视,对统一标准的重视和渴望。

遵循OpenTracing协议的产品有Jaeger、Zipkin等等。

OpenCensus

中国有句老话,既生瑜何生亮,OpenTracing本身出现的更早且更流行,为什么要有OpenCensus这个项目?

这里先补充一下背景知识,前面提到了分布式追踪,其实在APM领域,还有一个极其重要的监控子类:Metrics指标监控,例如cpu、内存、硬盘、网络等机器指标,grpc的请求延迟、错误率等网络协议指标,用户数、访问数、订单数等业务指标,都可以涵盖在内。

首先,该项目有个非常牛逼的亲爹:Google,要知道就连分布式跟踪的基础论文就是谷歌提出的,可以说谷歌就是亲爹无疑了。

其次,OpenCensus的最初目标并不是抢OpenTracing的饭碗,而是为了把Go语言的Metrics采集、链路跟踪与Go语言自带的profile工具打通,统一用户的使用方式。随着项目的进展,野心也膨胀了,这个时候开始幻想为什么不把其它各种语言的相关采集都统一呢?然后项目组发现了OpenTracing,突然发现,我K,作为谷歌,我们都没玩标准,你们竟然敢玩标准敢想着统一全世界?(此处乃作者的疯人疯语) 于是乎,OpenCensus的场景进一步扩大了,不仅做了Metrics基础指标监控,还做了OpenTracing的老本行:分布式跟踪。

有个谷歌做亲爹已经够牛了,那再加入一个微软做干爹呢?是不是要起飞了?所以,对于OpenCensus的发展而言,微软的直接加入可以说是打破了之前的竞争平衡,间接导致了后面OpenTelemetry项目的诞生。

正所谓是:天下合久必分、分久必合,在此之时,必有枭雄出现:OpenTelemetry横空出世。
两个产品合并,首先要考虑的是什么?有过经验的同学都知道:如何让两边的用户能够继续使用。因此新项目首要核心目标就是兼容OpenTracingOpenCensus

OpenTelemetry的核心工作目前主要集中在3个部分:

  1. 规范的制定和协议的统一,规范包含数据传输、API的规范,协议的统一包含:HTTP W3C的标准支持及GRPC等框架的协议标准
  2. 多语言SDK的实现和集成,用户可以使用SDK进行代码自动注入和手动埋点,同时对其他三方库(Log4j、LogBack等)进行集成支持;
  3. 数据收集系统的实现,当前是基于OpenCensus Service的收集系统,包括Agent和Collector。

由此可见,OpenTelemetry的自身定位很明确:数据采集和标准规范的统一,对于数据如何去使用、存储、展示、告警,官方是不涉及的,我们目前推荐使用Prometheus + GrafanaMetrics存储、展示,使用Jaeger做分布式跟踪的存储和展示。

在这里插入图片描述

大一统

有了以上的背景知识,我们就可以顶一下OpenTelemetry的终极目标了:实现Metrics、Tracing、Logging的融合及大一统,作为APM的数据采集终极解决方案。

  • Tracing:提供了一个请求从接收到处理完成整个生命周期的跟踪路径,一次请求通常过经过N个系统,因此也被称为分布式链路追踪
  • Metrics:例如cpu、请求延迟、用户访问数等Counter、Gauge、Histogram指标
  • Logging:传统的日志,提供精确的系统记录

这三者的组合可以形成大一统的APM解决方案:

  • 基于Metrics告警发现异常
  • 通过Tracing定位到具体的系统和方法
  • 根据模块的日志最终定位到错误详情和根源
  • 调整Metrics等设置,更精确的告警/发现问题

二、OpenTelemetry快速体验

jaeger自行安装

go快速体验

package main

import (
	"context"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	"time"
)

func main() {
	url := "http://127.0.0.1:14268/api/traces"
	//专门生成Exporter
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}
	//后续使用telemetry,和jaeger无关 ↓初始化
	tp := trace.NewTracerProvider(
		//上报器 批量上报处理链路
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)

	//自己创建context
	ctx, cancel := context.WithCancel(context.Background())
	//优雅退出
	defer func(ctx context.Context) {
		ctx, cancel = context.WithTimeout(ctx, time.Second*5)
		defer cancel()
		if err = tp.Shutdown(ctx); err != nil {
			panic(err)
		}
	}(ctx)

	//生成tracer
	tr := otel.Tracer("mxshop-otel")
	//生成span
	_, span := tr.Start(ctx, "func-main")
	//业务逻辑
	time.Sleep(time.Second)
	var attrs []attribute.KeyValue
	attrs = append(attrs, attribute.String("key1", "value1"))
	attrs = append(attrs, attribute.Bool("key2", false))
	attrs = append(attrs, attribute.Int("key2", 123))
	attrs = append(attrs, attribute.StringSlice("key2", []string{"value1", "value2"}))

	//设置span里的Tags键值对
	span.SetAttributes(attrs...)
	//设置logs
	span.AddEvent("this is an event")
	//业务逻辑
	time.Sleep(time.Second)
	//结束此span
	span.End()
}

效果:
在这里插入图片描述

OpenTelemetry系统架构

在这里插入图片描述

这其实是一个OpenTelemetry-collector的架构
Receiver可以适配不同的协议,其他厂商(jaeger)实现这个接口即可
processor默认的让他自动处理 剔除不需要的数据。也可以自己加入一些指标 pprof 健康检测等等
exporter是把这些数据打到什么地方,比如jaeger

官方提供的collector:https://github.com/open-telemetry/opentelemetry-collector
官方很少做适配,官方开放了一个仓库 其他云厂商会适配他的接口:
https://github.com/open-telemetry/opentelemetry-collector-contrib

尾部采样

可以到processor看看
高并发应该才会用到

全量采样

  • 优点:几乎可以分析出所有bug
  • 缺点:量大

尾部采样

  • 优点:尾部采样可以降低采样的开销,只对一部分请求进行采样,减少了数据的收集和传输成本。
  • 缺点:由于只对部分请求进行采样,采样结果可能不够全面和准确,可能会丢失某些有价值的数据。

1W请求:10个以内 10/100000000

采样率:100个采1个。优点 :量小。缺点:有可能某天你采集不到这个bug

采样:某个链路有错误必采,某个链路超时1S必采,就要在最后一个链路的时候才决定是否入库

100个span,99个span如果你就直接入库了,最后的一个入不入库已经不重要,如果一个processor可以在最后一个span产生的时候才决定是否将上个链路上报


三、通过http完成span传输

函数中传递span的context

go:

package main

import (
	"NewGo/log"
	"context"
	"encoding/json"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	"sync"
	"time"
)

const (
	traceName = "mxshop-otel"
)

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}
	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//设置传播提取器
	return nil
}
func funcA(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	tr := otel.Tracer(traceName)
	spCtx, span := tr.Start(ctx, "func-a")
	span.SetAttributes(attribute.String("name", "funA"))
	type _LogStruct struct {
		CurrentTime time.Time `json:"currentTime"`
		PassWho     string    `json:"passWho"`
		Name        string    `json:"name"`
	}
	logTest := _LogStruct{
		CurrentTime: time.Time{},
		PassWho:     "jzin",
		Name:        "func-a",
	}
	log.InfofC(spCtx, "is logs")
	b, _ := json.Marshal(logTest)
	log.InfofC(spCtx, string(b))
	span.SetAttributes(attribute.Key("测试key").String(string(b)))
	time.Sleep(time.Second)
	span.End()
}
func funcB(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	tr := otel.Tracer(traceName)
	spCtx, span := tr.Start(ctx, "func-b")
	span.SetAttributes(attribute.String("name", "funB"))
	type _LogStruct struct {
		CurrentTime time.Time `json:"currentTime"`
		PassWho     string    `json:"passWho"`
		Name        string    `json:"name"`
	}
	logTest := _LogStruct{
		CurrentTime: time.Time{},
		PassWho:     "bjzin",
		Name:        "func-b",
	}
	log.InfofC(spCtx, "is logs b")
	b, _ := json.Marshal(logTest)
	log.InfofC(spCtx, string(b))
	span.SetAttributes(attribute.Key("测试key").String(string(b)))
	time.Sleep(time.Second)
	span.End()
}
func main() {
	_ = tracerProvider()
	ctx, cancel := context.WithCancel(context.Background())
	defer func(ctx context.Context) {
		ctx, cancel = context.WithTimeout(ctx, time.Second*5)
		defer cancel()
		if err := tp.Shutdown(ctx); err != nil {
			panic(err)
		}
	}(ctx)
	tr := otel.Tracer(traceName)
	spanCtx, span := tr.Start(ctx, "func-main")
	wg := &sync.WaitGroup{}
	wg.Add(2)
	go funcA(spanCtx, wg)
	go funcB(spanCtx, wg)
	//设置logs
	span.AddEvent("this is an event")
	time.Sleep(time.Second)
	wg.Wait()
	span.End()
}

效果
在这里插入图片描述
http传输 gin接收
client:

package main

import (
	"NewGo/log"
	"context"
	"encoding/json"
	"fmt"
	"github.com/valyala/fasthttp"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	"sync"
	"time"
)

const (
	traceName = "mxshop-otel"
)

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}
	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//全局设置传播提取器
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return nil
}
func funcA(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	tr := otel.Tracer(traceName)
	spCtx, span := tr.Start(ctx, "func-a")
	span.SetAttributes(attribute.String("name", "funA"))
	type _LogStruct struct {
		CurrentTime time.Time `json:"currentTime"`
		PassWho     string    `json:"passWho"`
		Name        string    `json:"name"`
	}
	logTest := _LogStruct{
		CurrentTime: time.Time{},
		PassWho:     "jzin",
		Name:        "func-a",
	}
	log.InfofC(spCtx, "is logs")
	b, _ := json.Marshal(logTest)
	log.InfofC(spCtx, string(b))
	span.SetAttributes(attribute.Key("测试key").String(string(b)))
	time.Sleep(time.Second)
	span.End()
}
func funcB(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	tr := otel.Tracer(traceName)
	spanCtx, span := tr.Start(ctx, "func-b")
	span.SetAttributes(attribute.String("name", "funB"))
	fmt.Println("trace", span.SpanContext().TraceID(), span.SpanContext().SpanID())
	time.Sleep(time.Second)

	req := fasthttp.AcquireRequest()
	req.SetRequestURI("http://127.0.0.1:8090/server")
	req.Header.SetMethod("GET")

	//拿到传播器
	p := otel.GetTextMapPropagator()
	headers := make(map[string]string)
	//包裹   context信息注入到包裹里面 把trace的id span的id注入到包裹
	p.Inject(spanCtx, propagation.MapCarrier(headers))

	for key, value := range headers {
		req.Header.Set(key, value)
	}

	fclient := fasthttp.Client{}
	fres := fasthttp.Response{}
	err := fclient.Do(req, &fres)
	if err != nil {
		panic(err)
	}

	span.End()
}
func main() {
	_ = tracerProvider()
	ctx, cancel := context.WithCancel(context.Background())
	defer func(ctx context.Context) {
		ctx, cancel = context.WithTimeout(ctx, time.Second*5)
		defer cancel()
		if err := tp.Shutdown(ctx); err != nil {
			panic(err)
		}
	}(ctx)
	tr := otel.Tracer(traceName)
	spanCtx, span := tr.Start(ctx, "func-main")
	wg := &sync.WaitGroup{}
	wg.Add(2)
	go funcA(spanCtx, wg)
	go funcB(spanCtx, wg)
	//设置logs
	span.AddEvent("this is an event")
	time.Sleep(time.Second)
	wg.Wait()
	span.End()
}

server(gin实现):

package main

import (
	"github.com/gin-gonic/gin"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	"time"
)

const (
	traceName = "mxshop-otel"
)

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}
	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//全局设置传播提取器
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return nil
}
func Server(c *gin.Context) {
	//负责span的抽取和生成
	ctx := c.Request.Context()
	p := otel.GetTextMapPropagator()
	//生成新的context
	sCtx := p.Extract(ctx, propagation.HeaderCarrier(c.Request.Header))
	//拿到tracer
	tr := tp.Tracer(traceName)
	_, span := tr.Start(sCtx, "server")
	time.Sleep(time.Millisecond * 500)
	span.End()
	c.JSON(200, gin.H{})
}
func main() {
	_ = tracerProvider()
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{})
	})
	r.GET("/server", Server)
	if err := r.Run(":8090"); err != nil {
		panic(err)
	}
}

四、自定义inject和extract源码

自己实现http中traceID和spanID的传递

根据上述代码改进:

p.Inject(spanCtx, propagation.MapCarrier(headers))中headers的值为:

key="traceparent"
value="00-6be6f033385a7f6e8a56a3bbc71935c3-dc0c6e540abc7a95-01"

value中00和01之间是traceID和spanID
我们是可以自己设置key的。不用他这个propagation

自己设置前必须客户端和服务端达成一致
比如:我的key是tarceID 服务端就得解析tarceID 不能是其他的
直接设置header:

req.Header.Set("trace-id", span.SpanContext().TraceID().String())
req.Header.Set("span-id", span.SpanContext().SpanID().String())

server端解析:

func Server(c *gin.Context) {
	//负责span的抽取和生成
	ctx := c.Request.Context()
	tr := tp.Tracer(traceName)
	traceID := c.Request.Header.Get("trace-id")
	spanID := c.Request.Header.Get("span-id")
	traceid, _ := otelTrace.TraceIDFromHex(traceID)
	spanid, _ := otelTrace.SpanIDFromHex(spanID)
	spanCtx := otelTrace.NewSpanContext(otelTrace.SpanContextConfig{
		TraceID:    otelTrace.TraceID(traceid),
		SpanID:     otelTrace.SpanID(spanid),
		TraceFlags: otelTrace.FlagsSampled, //这个不设置的话是不会保存的
		Remote:     true,
	})

	//手动构造ctx
	carrier := propagation.HeaderCarrier{}
	carrier.Set("trace-id", traceID)
	propagattor := otel.GetTextMapPropagator()
	pctx := propagattor.Extract(ctx, carrier)
	sct := otelTrace.ContextWithRemoteSpanContext(pctx, spanCtx)
	//p := otel.GetTextMapPropagator()
	//生成新的context
	//sCtx := p.Extract(ctx, propagation.HeaderCarrier(c.Request.Header))
	//拿到tracer

	_, span := tr.Start(sct, "server")
	time.Sleep(time.Millisecond * 500)
	span.End()
	c.JSON(200, gin.H{})
}

五、gRPC集成

grpc官方维护的源码:https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/instrumentation/google.golang.org/grpc/otelgrpc

直接使用grpc维护的拦截器:
proto文件自行生成
client:

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/status"

	pb "NewGo/telemetry/rpc"
)

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}

	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//设置传播提取器
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return nil
}

func main() {
	_ = tracerProvider()
	// Set up a connection to the server.
	conn, err := grpc.Dial("localhost:50052",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
	)
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

	// Contact the server and print out its response.
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "world"})
	if err != nil {
		s, ok := status.FromError(err)
		if !ok {
			log.Fatalf("err is not standard grpc error: %v", err)
		}
		fmt.Println(s.Code())
		//log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetMessage())
}

server端:

这里返回的时候故意返回一个error看看效果

package main

import (
	"context"
	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"log"
	"net"

	pb "NewGo/error/rpc"
	"google.golang.org/grpc"
)

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))

	if err != nil {
		panic(err)
	}

	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//设置传播提取器
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return nil
}

// server is used to implement helloworld.GreeterServer.
type server struct {
	pb.UnimplementedGreeterServer
}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v", in.GetName())
	return nil, status.Error(codes.NotFound, "user not found")
	//return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
	_ = tracerProvider()
	lis, err := net.Listen("tcp", ":50052")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer(
		grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
	)
	pb.RegisterGreeterServer(s, &server{})
	log.Printf("server listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

效果:
在这里插入图片描述


「可无视这一章节」

自用框架集成(无视即可)

gmicro中trace的option是为了和业务option隔离开
虽然代码会重复 但是以后想要改trace的代码就不会影响业务代码

Sampler: 1.0 # 采样率 如果在源码中有其他设置 则会使用最后一次设置的

使用_, ok := agents[o.Endpoint]确定唯一性 因为一般是不会换地址的

//log中可以不加 是否允许把TraceID放进来,这样就可以看清楚是属于哪一个链路的
//这个逻辑可以适用于 如果没有这个traceid 表示不上报到jaeger中 那我就打印到本地文件,自行改代码
if l.withTraceID {
traceID := span.SpanContext().TraceID().String()
fields = append(fields, zap.String(“trace_id”, traceID))
}


六、log集成

把自定义的log附着在span上:

自用的自定义的log包(源码在末尾):https://blog.csdn.net/the_shy_faker/article/details/129420308

只需要把spCtx, span := tr.Start(ctx, "func-a")中的spCtx传递到我用的log包中即可:

package main

import (
	"NewGo/log"
	"context"
	"encoding/json"
	"fmt"
	"github.com/valyala/fasthttp"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	"sync"
	"time"
)

const (
	traceName = "mxshop-otel"
)

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}
	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//全局设置传播提取器
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return nil
}
func funcA(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	tr := otel.Tracer(traceName)
	spCtx, span := tr.Start(ctx, "func-a")
	span.SetAttributes(attribute.String("name", "funA"))
	type _LogStruct struct {
		CurrentTime time.Time `json:"currentTime"`
		PassWho     string    `json:"passWho"`
		Name        string    `json:"name"`
	}
	logTest := _LogStruct{
		CurrentTime: time.Time{},
		PassWho:     "jzin",
		Name:        "func-a",
	}
	log.InfofC(spCtx, "is logs")
	b, _ := json.Marshal(logTest)
	log.InfofC(spCtx, string(b))
	span.SetAttributes(attribute.Key("测试key").String(string(b)))
	time.Sleep(time.Second)
	span.End()
}
func funcB(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	tr := otel.Tracer(traceName)
	spanCtx, span := tr.Start(ctx, "func-b")
	span.SetAttributes(attribute.String("name", "funB"))
	fmt.Println("trace", span.SpanContext().TraceID(), span.SpanContext().SpanID())
	time.Sleep(time.Second)

	req := fasthttp.AcquireRequest()
	req.SetRequestURI("http://127.0.0.1:8090/server")
	req.Header.SetMethod("GET")

	//拿到传播器
	p := otel.GetTextMapPropagator()
	//包裹   context信息注入到包裹里面 把trace的id span的id注入到包裹
	p.Inject(spanCtx, propagation.HeaderCarrier{})
	headers := make(map[string]string)
	//包裹   context信息注入到包裹里面 把trace的id span的id注入到包裹
	p.Inject(spanCtx, propagation.MapCarrier(headers))

	for key, value := range headers {
		req.Header.Set(key, value)
	}

	//req.Header.Set("trace-id", span.SpanContext().TraceID().String())
	//req.Header.Set("span-id", span.SpanContext().SpanID().String())

	fclient := fasthttp.Client{}
	fres := fasthttp.Response{}
	_ = fclient.Do(req, &fres)

	span.End()
}
func main() {
	_ = tracerProvider()
	ctx, cancel := context.WithCancel(context.Background())
	defer func(ctx context.Context) {
		ctx, cancel = context.WithTimeout(ctx, time.Second*5)
		defer cancel()
		if err := tp.Shutdown(ctx); err != nil {
			panic(err)
		}
	}(ctx)
	tr := otel.Tracer(traceName)
	spanCtx, span := tr.Start(ctx, "func-main")
	wg := &sync.WaitGroup{}
	wg.Add(1)
	go funcA(spanCtx, wg)
	//go funcB(spanCtx, wg)
	//设置logs
	span.AddEvent("this is an event")
	time.Sleep(time.Second)
	wg.Wait()
	span.End()
}

由于gin的context和普通的context不一样
gin中真正的context是放在ctx.(*gin.Context).Request.Context()中的
如果传递的是gin中的context log包里会提取出context

效果:
在这里插入图片描述


七、gorm集成

官方维护的集成telemetry源码:https://github.com/go-gorm/opentelemetry/tree/master/examples/demo

注意提前建好库
测试示例:

package main

import (
	"log"
	"os"
	"time"

	"github.com/gin-gonic/gin"
	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	otelTrace "go.opentelemetry.io/otel/trace"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"
	"gorm.io/plugin/opentelemetry/tracing"
)

const (
	traceName = "mxshop-otel"
)

type BaseModel struct {
	ID        int32          `gorm:"primary_key;comment:ID"`
	CreatedAt time.Time      `gorm:"column:add_time;comment:创建时间"`
	UpdatedAt time.Time      `gorm:"column:update_time;comment:更新时间"`
	DeletedAt gorm.DeletedAt `gorm:"comment:删除时间"`
	IsDeleted bool           `gorm:"comment:是否删除"`
}
type User struct {
	BaseModel
	Mobile   string     `gorm:"index:idx_mobile;unique;type:varchar(11);not null;comment:手机号"`
	Password string     `gorm:"type:varchar(100);not null;comment:密码"`
	NickName string     `gorm:"type:varchar(20);comment:账号名称"`
	Birthday *time.Time `gorm:"type:datetime;comment:出生日期"`
	Gender   string     `gorm:"column:gender;default:male;type:varchar(6);comment:femail表示女,male表示男"`
	Role     int        `gorm:"column:role;default:1;type:int;comment:1表示普通用户,2表示管理员"`
}

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}

	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//设置传播提取器
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return nil
}

func Server2(c *gin.Context) {
	dsn := "root:123456@tcp(localhost:3306)/mxshop_user_srv2?charset=utf8mb4&parseTime=True&loc=Local"
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags),
		logger.Config{
			LogLevel: logger.Info,
			Colorful: true,
		},
	)
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true,
		},
		Logger: newLogger,
	})
	if err != nil {
		panic(err)
	}
	//之前初始化好了*trace.TracerProvider这里就不用再初始化了
	if err := db.Use(tracing.NewPlugin()); err != nil {
		panic(err)
	}

	//负责span的抽取和生成
	//如果使用中间件otelgin.Middleware 它会把自己trace span,把context放到c.Request.Context()中
	//ctx := c.Request.Context()
	//p := otel.GetTextMapPropagator()
	//tr := tp.Tracer(traceName)
	//sCtx := p.Extract(ctx, propagation.HeaderCarrier(c.Request.Header))
	//spanCtx, span := tr.Start(sCtx, "server")

	if err := db.WithContext(c.Request.Context()).Model(User{BaseModel: BaseModel{ID: 12}}).First(&User{}).Error; err != nil {
		panic(err)
	}

	time.Sleep(500 * time.Millisecond)
	//span.End()
	c.JSON(200, gin.H{})

}

func main() {
	_ = tracerProvider()
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{})
	})
	r.GET("/server", Server2)
	err := r.Run(":8090")
	if err != nil {
		return
	}
}

效果:
在这里插入图片描述


八、gin集成

官方维护的源码:https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/instrumentation/github.com

加一个官方维护的中间件即可:

package main

import (
	"log"
	"os"
	"time"

	"github.com/gin-gonic/gin"
	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
	otelTrace "go.opentelemetry.io/otel/trace"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"
	"gorm.io/plugin/opentelemetry/tracing"
)

const (
	traceName = "mxshop-otel"
)

type BaseModel struct {
	ID        int32          `gorm:"primary_key;comment:ID"`
	CreatedAt time.Time      `gorm:"column:add_time;comment:创建时间"`
	UpdatedAt time.Time      `gorm:"column:update_time;comment:更新时间"`
	DeletedAt gorm.DeletedAt `gorm:"comment:删除时间"`
	IsDeleted bool           `gorm:"comment:是否删除"`
}
type User struct {
	BaseModel
	Mobile   string     `gorm:"index:idx_mobile;unique;type:varchar(11);not null;comment:手机号"`
	Password string     `gorm:"type:varchar(100);not null;comment:密码"`
	NickName string     `gorm:"type:varchar(20);comment:账号名称"`
	Birthday *time.Time `gorm:"type:datetime;comment:出生日期"`
	Gender   string     `gorm:"column:gender;default:male;type:varchar(6);comment:femail表示女,male表示男"`
	Role     int        `gorm:"column:role;default:1;type:int;comment:1表示普通用户,2表示管理员"`
}

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}

	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//设置传播提取器
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return nil
}

func Server2(c *gin.Context) {
	dsn := "root:123456@tcp(localhost:3306)/mxshop_user_srv2?charset=utf8mb4&parseTime=True&loc=Local"
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags),
		logger.Config{
			LogLevel: logger.Info,
			Colorful: true,
		},
	)
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true,
		},
		Logger: newLogger,
	})
	if err != nil {
		panic(err)
	}
	//之前初始化好了*trace.TracerProvider这里就不用再初始化了
	if err = db.Use(tracing.NewPlugin()); err != nil {
		panic(err)
	}

	//负责span的抽取和生成
	//如果使用中间件otelgin.Middleware 它会把自己trace span,把context放到c.Request.Context()中
	//ctx := c.Request.Context()
	//p := otel.GetTextMapPropagator()
	//tr := tp.Tracer(traceName)
	//sCtx := p.Extract(ctx, propagation.HeaderCarrier(c.Request.Header))
	//spanCtx, span := tr.Start(sCtx, "server")

	if err = db.WithContext(c.Request.Context()).Model(User{BaseModel: BaseModel{ID: 12}}).First(&User{}).Error; err != nil {
		panic(err)
	}

	time.Sleep(500 * time.Millisecond)
	//span.End()
	c.JSON(200, gin.H{})

}
func main() {
	_ = tracerProvider()
	r := gin.Default()
	//添加trace中间件
	r.Use(otelgin.Middleware("my-server"))
	r.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{})
	})
	r.GET("/server", Server2)
	err := r.Run(":8090")
	if err != nil {
		return
	}
}

效果:
在这里插入图片描述


九、redis集成

官方维护的redis源码:https://github.com/redis/go-redis/tree/master/extra/redisotel

官方文档里有说明 这里直接用:

package main

import (
	"context"
	"github.com/redis/go-redis/extra/redisotel/v9"
	"github.com/redis/go-redis/v9"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
)

const (
	traceName = "mxshop-otel"
)

var tp *trace.TracerProvider

func tracerProvider() error {
	url := "http://127.0.0.1:14268/api/traces"
	jexp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		panic(err)
	}

	//上报器 批量处理链路追踪器
	tp = trace.NewTracerProvider(
		trace.WithBatcher(jexp),
		//如果未使用此选项,跟踪程序提供程序将使用该资源 默认资源。
		trace.WithResource(
			resource.NewWithAttributes(
				//固定写法
				semconv.SchemaURL,
				//设置service
				semconv.ServiceNameKey.String("mxshop-user"),
				//设置Process键值对 可以让其他人员分析 全局的,设置到trace上的
				attribute.String("environment", "dev"),
				attribute.Int("ID", 1),
			),
		),
	)
	otel.SetTracerProvider(tp)
	//设置传播提取器
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return nil
}

func main() {
	_ = tracerProvider()
	cli := redis.NewClient(&redis.Options{
		Addr: "127.0.0.1:端口",
	})
	// Enable tracing instrumentation.
	if err := redisotel.InstrumentTracing(cli); err != nil {
		panic(err)
	}
	tr := otel.Tracer(traceName)
	spanCtx, span := tr.Start(context.Background(), "redis")
	cli.Set(spanCtx, "name", "jzin", 0)
	span.End()
	err := tp.Shutdown(context.Background())
	if err != nil {
		return
	}
}

效果:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/560196.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

最强chrome、edge的广告拦截插件

最强chrome、edge的广告拦截插件 对于浏览器的广告是非常烦人的,尤其是使用百度搜索引擎时,总会在右侧出现百度热搜这样的东西,有时不经意间就会被某些热搜吸引。本来想搜索一些内容,结果被热搜等耗费了大量时间。 有很多人推荐…

pytorch基础语法学习:数据预处理transforms模块

来源:投稿 作者:阿克西 编辑:学姐 建议搭配视频食用 视频链接:https://ai.deepshare.net/detail/p_5df0ad9a09d37_qYqVmt85/6 系列其他文章传送门: pytorch基础语法学习:数据读取机制Dataloader与Datase…

3D点云数据转为俯瞰图Python实现代码

我主要是参考了英文博客来撰写本篇文章,仅作为个人学习笔记参考使用。 文章目录 一、点云数据二、图像与点云坐标三、创建点云数据的鸟瞰视图3.1 鸟瞰图的相关坐标轴3.2 限制点云数据范围3.3 将点位置映射到像素位置3.4 切换到新的零点3.5 像素值3.6 创建图像矩阵3.…

IOS最新版开通GPT-PLUS方法

前提,美国IP魔法 不多说了 1.拥有一个美区apple id账号 可以买,也可以自己申请 自己申请就打开魔法到apple官网注册,用gmail邮箱,然后地址用美国地址生成器,记得选免税州 2.充值礼品卡 支付宝可以充值礼品卡&…

大模型总是「胡说八道」怎么办?手把手教你如何应对!

随着 ChatGPT 的出现,「AI 幻觉」一词被频繁提及。那么,什么是 AI 幻觉?简单来说,就是大模型在一本正经地胡说八道。 不止 ChatGPT,其他大语言模型也经常如此,究其根本是大语言模型在训练的过程中存在数据偏…

驱动开发-----io模型总结(2023-5-23)

1.非阻塞模型 在我们使用open函数时,将打开的驱动设置为O_NONBLOCK时,当我们用read函数去读取硬件数据时,无论硬件是否有数据,都会往下执行,不会被阻塞在这里 2.阻塞模型 在我们使用open函数时,没有设置…

C++学习之路-变量和基本内置类型

变量和基本内置类型 一、基本内置类型1.1 算数类型1.2 带符号类型和无符号类型1.3 类型转换含有无符号类型的表达式 1.4 字面值常量整形和浮点型字面值字符和字符串字面值转义序列指定字面值的类型 二、变量2.1 变量的定义初始化列表初始化默认初始化 2.2 变量声明和定义的关系…

斐波那契数列数列相关简化1

斐波那契数列问题介绍: 斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”&#xf…

包管理工具详解npm、yarn、cnpm、npx、pnpm

目录: 1 npm包管理工具 2 package配置文件 3 npm install原理 4 yarn、cnpm、npx 5 发布自己的开发包 6 pnpm使用和原理 当我们使用npm install xxxx 的时候会添加一个node_module和2个json文件: package.json是配置信息文件,  这个配…

Go完整即时通讯项目及Go的生态介绍

Go完整即时通讯项目 项目架构: 1 编写基本服务端-Server server.go package mainimport ("fmt""net" )// 定义服务端 type Server struct {ip stringport int }// 创建一个Server func NewServer(ip string, port int) *Server {return …

Jenkins + docker-compose 在 Centos 上搭建部署

一、前期准备 1. 检查 CentOS上 是否安装 docker 可以使用以下命令: sudo docker version 如果已经安装了Docker,它将显示有关Docker版本和构建信息的输出。如果未安装Docker,将收到有关命令未找到的错误消息。 2. 检查是否安装 docker-…

cookie-机制

目录 一、基础概念 二、cookie的处理方式 一、基础概念 1、cookie是存储在客户端的一组键值对 2、web中cookie的典型应用:免密登陆 3、cookie和爬虫之间的关联 有时,对一张页面进行请求的时候,如果请求的过程中不携带cookie的话&#xf…

Openai+Coursera: ChatGPT Prompt Engineering(四)

想和大家分享一下最近学习的Coursera和openai联合打造ChatGPT Prompt Engineering在线课程.以下是我写的关于该课程的前两篇博客: ChatGPT Prompt Engineering(一)ChatGPT Prompt Engineering(二)ChatGPT Prompt Engineering(三) 今天我们来学习第三部分内容&…

Java on Azure Tooling 4月更新|路线图更新及 Azure Toolkit for IntelliJ 增强

作者:Jialuo Gan - Program Manager, Developer Division at Microsoft 排版:Alan Wang 大家好,欢迎来到 Java on Azure 工具产品的4月更新。让我们首先来谈谈我们对未来几个月的 Java on Azure 开发工具的投资。在这次更新中,我们…

js - 闭包

1、闭包的概念 闭包:函数嵌套函数,内层函数访问了外层函数的局部变量。 // 闭包 function func1() {let a 9;let b 8;function func2() {console.log("a", a); // a 9}func2(); } func1(); 分析: 需要访问的变量会被放到闭包…

【云原生|Kubernetes】05-Pod的存储卷(Volume)

【云原生Kubernetes】05-Pod的存储卷(Volume) 文章目录 【云原生Kubernetes】05-Pod的存储卷(Volume)简介Volume类型解析emptyDirHostPathgcePersistentDiskNFSiscsiglusterfsceph其他volume 简介 Volume 是Pod 中能够被多个容器访问的共享目录。 Kubern…

ChatGPT可以帮助开发人员的8种方式...

“适应或灭亡”是科技界的口头禅,如果您是开发人员,则尤其如此。 由于技术的动态发展,开发人员面临着比大多数人更大的压力,他们要领先于适应和精通最好的工具。ChatGPT 是最新的此类工具。 虽然有人说 ChatGPT 是“工作杀手”&…

比Figma更丝滑的“Figma网页版“

随着互联网的全面普及和全球化,设计协作工具逐渐成为团队协作中不可或缺的一部分。设计师们常需要通过在线设计协作工具来完成设计任务,而 Figma 作为协作工具的佼佼者,成为了许多设计师心中的首选。但是,对于国内设计师来说&…

Leetcode406. 根据身高重建队列

Every day a Leetcode 题目来源:406. 根据身高重建队列 解法1:贪心 题解:根据身高重建队列 我们先按照身高从大到小排序(身高相同的情况下K小的在前面),这样的话,无论哪个人的身高都小于等于…

kubeadm安装集群的时候kube-proxy是如何安装的

背景 最近升级k8s集群时遇到这个问题,集群是使用kuberadm自动化脚本安装的,之前一直认为kubeadm安装的集群这些组件除了kubelet都是静态pod跑起来的。 其实kube-proxy并不是. kube-proxy是如何安装的 在使用kubeadmin安装Kubernetes集群时&#xff0c…