使用GO和Tracetest的可观察性驱动开发
#教程 #devops #go #observability

我们进入了observability-driven development的新时代。 ODD使用OpentElemetry仪器作为测试中的断言!

Here's在Twitter上的一个很棒的解释!

这正在推动trace-based testing的新文化。通过基于痕量的测试,您可以从基于OpentElemetry的痕迹中生成集成测试,执行质量,鼓励速度并增加微服务和分布式应用程序的测试覆盖范围。

今天,您将学习使用GO和Docker构建分布式系统。您可以使用opentelemetry traces进行仪器,然后使用TraceTest在OpenTelemetry基础结构之上运行基于跟踪的测试。

https://res.cloudinary.com/djwdcmwdz/image/upload/v1671802096/Blogposts/observability-driven-development-with-go-and-tracetest/screely-1671802081166_b2euer.png

我们将遵循可观察性驱动的发展原则,并展示为什么在当今云中开发分布式系统的世界中,为什么它强大。

在本教程结束时,您将学习以观察性驱动的开发,如何使用GO开发微服务以及如何使用Tracetest进行基于跟踪的测试。

检查entire code, jump over to GitHub

我们正在建造什么?

我们构建的分布式应用程序将有两个微服务和一个用于OpenTelemetry Trace仪器的专用模块。

我们将使用编写代码,并撰写Docker来部署微服务。该应用程序本身是一家书店,以其价格和可用性显示书籍。

我们将遵循以可观察性驱动的开发的开发最佳实践,然后编写代码并添加OpenTelemetry仪器以验证测试规范,并最终使用TraceTest进行基于跟踪的测试,并确保它们通过。

教程由3个部分组成。

第一部分将致力于配置基本的书店基础架构和书籍服务,设置OpentElemetry仪器并安装Tracetest。

在第二部分中,我们将专注于操作奇数测试并为书籍服务创建基于跟踪的测试。

第三个也是最后一部分将集中于创建可用性服务并通过测试介绍。

在开始之前,让我们快速解释opentelemetry和traceTest是什么。

什么是opentelemetry?

OpenTelemetry是一个可观察性框架,可帮助生成和捕获云本地软件的遥测数据。

opentelemetry收集可观察性数据,包括痕迹,指标和日志。

OpentElemetry是一个社区驱动的开源项目,截至2021年8月,CNCF孵化项目。 OpentElemetry是Kubernetes背后的second most active CNCF project

这是我们在本指南中使用的三个组件:

因为OpentElemetry是一个框架,因此您需要数据存储来持续痕迹。我们将如何将Jaeger用作跟踪数据存储和OpenTelemetry Collector作为通往Jaeger的途径的门户。

什么是TraceTest?

Tracetest使用您现有的OpenTelemetry痕迹在请求交易的每个点上对您的跟踪数据进行断言,从而为基于跟踪的测试提供供电。

您将traceTest to to Point to to现有的Jaeger跟踪数据源。然后,TraceTest将在运行集成测试时从Jaeger中取出痕迹,以对跟踪数据本身运行断言。

有一个完全使用跟踪数据存储绕过的选项,并通过将opentelemetry collector配置为explained in our docs

这样,我们准备开始编码!

设置基础架构以可观察性驱动的开发

本节将解释书店应用程序和Tracetest的初始配置。我们将使用Docker组成的基础架构。完成后,您将拥有一个配置用于运行测试的Tracetest的运行应用程序。

安装traceTest用于本地开发

您可以关注sample code we’ve prepared for part 1 of this tutorial。遵循以下说明:

git clone git@github.com:kubeshop/tracetest.git
cd examples/observability-driven-development-go-tracetest/bookstore/part1
docker compose -f docker-compose.yaml -f tracetest/docker-compose.yaml up

注意:查看我们observability-driven development video tutorial, here的第一部分!

让我们逐步穿过安装。

从安装Tracetest CLI开始。这是最简单的traceTest。

brew install kubeshop/tracetest/tracetest

注意:关注this guide为您的特定操作系统安装。

安装了CLI后,创建一个名为bookstore的目录,然后安装Tracetest Server。

tracetest server install

遵循提示,并以traceTest安装裸露的设置。这将生成一个空的docker-compose.yaml文件和一个包含另一个docker-compose.yaml./tracetest/目录。

用traceTest配置OpenTelemetry收集器和Jaeger

让S编辑docker-compose.yaml添加OpenTelemetry Collector和Jaeger。

# ./tracetest/docker-compose.yaml

services:
    jaeger:
        healthcheck:
            test:
                - CMD
                - wget
                - --spider
                - localhost:16686
            timeout: 3s
            interval: 1s
            retries: 60
        image: jaegertracing/all-in-one:latest
        networks:
            default: null
        restart: unless-stopped
    otel-collector:
        command:
            - --config
            - /otel-local-config.yaml
        depends_on:
            jaeger:
                condition: service_started
        image: otel/opentelemetry-collector:0.54.0
        networks:
            default: null
        volumes:
            - type: bind
              source: tracetest/otel-collector.yaml
              target: /otel-local-config.yaml
              bind:
                create_host_path: true
    postgres:
        environment:
            POSTGRES_PASSWORD: postgres
            POSTGRES_USER: postgres
        healthcheck:
            test:
                - CMD-SHELL
                - pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"
            timeout: 5s
            interval: 1s
            retries: 60
        image: postgres:14
        networks:
            default: null
    tracetest:
        depends_on:
            otel-collector:
                condition: service_started
            postgres:
                condition: service_healthy
        extra_hosts:
            host.docker.internal: host-gateway
        healthcheck:
            test:
                - CMD
                - wget
                - --spider
                - localhost:11633
            timeout: 3s
            interval: 1s
            retries: 60
        image: kubeshop/tracetest:v0.9.3
        networks:
            default: null
        ports:
            - mode: ingress
              target: 11633
              published: 11633
              protocol: tcp
        volumes:
            - type: bind
              source: tracetest/tracetest.yaml
              target: /app/config.yaml
networks:
    default:
        name: _default

让我解释docker-compose.yaml文件中发生了什么:

  • 我们将OpenTelemetry收集器连接起来,充当我们应用程序将生成的所有跟踪的网关,并作为跟踪数据存储。
  • OpenTelemetry收集器将从我们的GO微服务中接收所有痕迹,并将其发送到Jaeger。
  • 然后,我们将在运行基于跟踪的测试时配置TraceTest从Jaeger获取数据。

确保您的traceTest的配置文件和opentelemetry收集器与示例代码匹配。首先将其复制到您的otel-collector.yaml中。

# ./tracetest/otel-collector.yaml

exporters:
    jaeger:
        endpoint: jaeger:14250
        tls:
            insecure: true
processors:
    batch:
        timeout: 100ms
receivers:
    otlp:
        protocols:
            grpc: null
            http: null
service:
    pipelines:
        traces:
            exporters:
                - jaeger
            processors:
                - batch
            receivers:
                - otlp

现在,从bookstore目录中,启动docker撰写以测试traceTest安装。

docker compose -f docker-compose.yaml -f tracetest/docker-compose.yaml up

此命令将旋转基础架构,并在11633端口上暴露Tracetest。在您的浏览器中打开http://localhost:11633/

配置跟踪数据存储在Web UI中指向Jaeger。

您也可以configure Jaeger via the CLI

添加books微服务

bookstore目录中,创建一个books目录,然后初始化一个go模块。

cd ./books
go mod init github.com/your-username/bookstore/books

books目录中创建一个main.go文件。将此代码粘贴到main.go

// ./books/main.go

package main

import (
    "context"
    "fmt"
    "io"
    "log"
    "net/http"
    "time"

    "github.com/gorilla/mux"
    "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/propagation"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
    "go.opentelemetry.io/otel/trace"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

const svcName = "books"

var tracer trace.Tracer

func newExporter(ctx context.Context) (sdktrace.SpanExporter, error) {
    ctx, cancel := context.WithTimeout(ctx, time.Second)
    defer cancel()
    conn, err := grpc.DialContext(ctx, "otel-collector:4317", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock())
    if err != nil {
        return nil, fmt.Errorf("failed to create gRPC connection to collector: %w", err)
    }

    traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))
    if err != nil {
        return nil, fmt.Errorf("failed to create trace exporter: %w", err)
    }

    return traceExporter, nil
}

func newTraceProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider {
    // Ensure default SDK resources and the required service name are set.
    r, err := resource.Merge(
        resource.Default(),
        resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceNameKey.String(svcName),
        ),
    )

    if err != nil {
        panic(err)
    }

    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exp),
        sdktrace.WithResource(r),
    )

    otel.SetTextMapPropagator(
        propagation.NewCompositeTextMapPropagator(
            propagation.TraceContext{},
            propagation.Baggage{},
        ),
    )

    return tp
}

func main() {
    ctx := context.Background()

    exp, err := newExporter(ctx)
    if err != nil {
        log.Fatalf("failed to initialize exporter: %v", err)
    }

    // Create a new tracer provider with a batch span processor and the given exporter.
    tp := newTraceProvider(exp)

    // Handle shutdown properly so nothing leaks.
    defer func() { _ = tp.Shutdown(ctx) }()

    otel.SetTracerProvider(tp)

    // Finally, set the tracer that can be used for this package.
    tracer = tp.Tracer(svcName)

    r := mux.NewRouter()
    r.Use(otelmux.Middleware(svcName))

    r.HandleFunc("/books", booksListHandler)

    http.Handle("/", r)

    log.Fatal(http.ListenAndServe(":8001", nil))
}

func booksListHandler(w http.ResponseWriter, r *http.Request) {
    _, span := tracer.Start(r.Context(), "Books List")
    defer span.End()

    io.WriteString(w, "Hello!\n")
}

让我们走过./books/main.go文件中发生的事情:

  • newExporter函数正在定义如何导出跟踪数据并将其转发到我们在otel-collector:4317上运行的OpenTelemetry收集器。
  • newTraceProvider功能正在初始化我们用来仪器代码的示踪剂。
  • 主要功能是初始化所有内容,并定义了一个称为/books的HTTP路由来触发booksListHandler
  • booksListHandler函数将返回一个简单的"Hello!"字符串。它还启动了OpenTelemetry Tracer并定义了一个称为"Books List"的跨度。

随着所有这些添加,通过从books目录中运行此命令来获取GO依赖项:

go mod tidy

这将生成一个go.sum文件。

最后,在bookstore目录中的docker-compose.yaml文件中添加books服务。这是root docker-compose.yaml文件,而不是./tracetest/目录中的文件。

# ./docker-compose.yaml

services:
  books:
    image: your_username/books
    build:
      args:
        SERVICE: books
    ports:
      - 8001:8001
    depends_on:
      otel-collector:
        condition: service_started

接下来,创建一个Dockerfile并将此代码粘贴到其中:

# ./Dockerfile

FROM golang:1.19

ARG SERVICE

WORKDIR /app/${SERVICE}

COPY ./${SERVICE}/go.* /app/${SERVICE}
RUN go mod download

COPY ./${SERVICE}/* /app/${SERVICE}
RUN go build -o /app/server .

ENTRYPOINT [ "/app/server" ]

最后,重新启动Docker撰写以尝试books服务。

docker compose -f docker-compose.yaml -f tracetest/docker-compose.yaml up

在Tracetest Web UI中运行基于跟踪的测试

使用端口11633上运行的tracetest服务,在浏览器中的http://localhost:11633/上打开它。

https://res.cloudinary.com/djwdcmwdz/image/upload/v1672045450/Blogposts/observability-driven-development-with-go-and-tracetest/screely-1672045442359_i6bx8r.png

创建一个新的HTTP测试。给它一个名字,并确保将URL设置为http://books:8001/books

https://res.cloudinary.com/djwdcmwdz/image/upload/v1672045846/Blogposts/observability-driven-development-with-go-and-tracetest/screely-1672045826154_sgl1re.png

单击创建。这将触发测试立即运行。

https://res.cloudinary.com/djwdcmwdz/image/upload/v1672045925/Blogposts/observability-driven-development-with-go-and-tracetest/screely-1672045918599_aano1x.png

测试将返回200状态代码。接下来,我们需要添加针对跟踪数据的断言,以确保我们的opentelemetry跟踪仪器在我们的GO代码中起作用。

打开Trace选项卡,让我们从添加状态代码断言开始。单击Tracetest trigger跨度。在左导航中,选择tracetest.response.status,然后单击Create test spec。如果您手工编写断言,请确保在选择要断言的属性时,用attr:将属性启动。

https://res.cloudinary.com/djwdcmwdz/image/upload/v1672046199/Blogposts/observability-driven-development-with-go-and-tracetest/screely-1672046190788_jesaei.png

保存测试规范,并向Books list跨度添加另一个断言。这次添加称为attr:tracetest.selected_spans.count = 1的属性。

https://res.cloudinary.com/djwdcmwdz/image/upload/v1672046555/Blogposts/observability-driven-development-with-go-and-tracetest/screely-1672046532214_knnl08.png

保存并发布测试规格。重新运行测试。

https://res.cloudinary.com/djwdcmwdz/image/upload/v1672046663/Blogposts/observability-driven-development-with-go-and-tracetest/screely-1672046656385_wjjtws.png

您现在有通过测试来确保服务使用200状态代码响应并验证OpenTelemetry手册代码仪器的作品!

使用Tracetest CLI运行基于跟踪的测试

让我们的追踪,不打算双关语,我们使用Tracetest Cli的步骤。

bookstore目录中创建一个e2e目录。创建一个称为books-list.yaml的文件。这将包含我们将使用CLI触发的测试定义。

将此代码粘贴到books-list.yaml

# ./e2e/books-list.yaml

type: Test
spec:
  id: k6hEWU54R
  name: Books Listing
  description: Try books service
  trigger:
    type: http
    httpRequest:
      url: http://books:8001/books
      method: GET
      headers:
      - key: Content-Type
        value: application/json
  specs:
    - selector: span[name="Tracetest trigger"]
      assertions:
        - attr:tracetest.response.status = 200
    - selector: span[name="Books List"]
      assertions:
        - attr:tracetest.selected_spans.count = 1

花点时间阅读代码。您会看到主张与我们刚刚在Tracetest Web UI中添加的内容相匹配。

要从命令行触发测试,首先配置Tracetest CLI。确保将CLI指向TraceTest服务正在运行的URL。在这个样本中,它是http://localhost:11633/

tracetest configure

[Output]
Enter your Tracetest server URL [http://localhost:11633]: http://localhost:11633

[Output]
Enable analytics? [Y/n]: Yes

现在,我们可以运行测试。从bookstore dir,运行:

tracetest test run -d ./e2e/books-list.yaml -w

[Output]
✔ Books Listing (http://localhost:11633/test/k6hEWU54R/run/2/test)

单击链接将打开Web UI中的测试运行。

完成了初始设置后,我们准备继续前进,并通过可观察性驱动的开发进行操作!

动手可观察性驱动的发展

跟随,您可以check out the sample code we’ve prepared for part 2.1。遵循以下说明:

git clone git@github.com:kubeshop/tracetest.git
cd examples/observability-driven-development-go-tracetest/bookstore/part2.1
docker compose -f docker-compose.yaml -f tracetest/docker-compose.yaml up
###
tracetest test run -d ./e2e/books-list.yaml -w

注意:查看我们observability-driven development video tutorial, here的第二部分!

开始,让我们首先在books-list.yaml中添加更多详细的断言,并使我们的测试失败。

打开books-list.yaml文件,并添加一个称为attr:books.list.count的自定义属性。这意味着我们希望Books List API测试返回3本书。

# ./e2e/books-list.yaml

# ...

  specs:
    - selector: span[name="Tracetest trigger"]
      assertions:
        - attr:tracetest.response.status = 200
    - selector: span[name="Books List"]
      assertions:
        - attr:tracetest.selected_spans.count = 1
        - attr:books.list.count = 3

跳回终端,再次运行测试。

tracetest test run -d ./e2e/books-list.yaml -w

[Output]
✘ Books Listing (http://localhost:11633/test/k6hEWU54R/run/1/test)
    ✔ span[name="Tracetest trigger"]#ebae1f382ecb81f6
            ✔ attr:tracetest.response.status = 200 (200)
    ✘ span[name="Books List"]#f6c5fa3aa5527a7a
            ✔ attr:tracetest.selected_spans.count = 1 (1)
            ✘ attr:books.list.count = 3 (http://localhost:11633/test/k6hEWU54R/run/1/test?selectedAssertion=1&selectedSpan=f6c5fa3aa5527a7a)

"Books List"跨度现在未能进行测试。

以真正的奇数方式,让我们添加代码以满足测试规格。

我们需要添加getBooks功能来检索书籍,并确保添加OpenTelemetry仪器以验证它确实以数组的形式返回书籍。

打开./books/main.go文件。我们将编辑booksListHandler函数,并添加一个getBooks函数,该功能模拟从数据库中获取书籍。

// ./books/main.go

// ...

func booksListHandler(w http.ResponseWriter, r *http.Request) {
    ctx, span := tracer.Start(r.Context(), "Books List")
    defer span.End()

    books, err := getBooks(ctx)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        io.WriteString(w, "cannot read books DB")
        return
    }

  // This is how we instrument the code with OpenTelemetry
  // This is the attribute we run the assertion against
    span.SetAttributes(
        attribute.Int("books.list.count", len(books)),
    )

    jsonBooks, err := json.Marshal(books)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        io.WriteString(w, "cannot json encode books DB")
        return
    }

    w.Write(jsonBooks)
}

type book struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Price int    `json:"price"`
}

// Mocking a database request
func getBooks(ctx context.Context) ([]book, error) {
    return []book{
        {"1", "Harry Potter", 0},
        {"2", "Foundation", 0},
        {"3", "Moby Dick", 0},
    }, nil
}

保存更改,然后重新启动Docker组成。现在,运行相同的测试。

tracetest test run -d ./e2e/books-list.yaml -w

[Output]
✔ Books Listing (http://localhost:11633/test/k6hEWU54R/run/1/test)

测试通过。单击测试中的链接将打开Tracetest Web UI。

https://res.cloudinary.com/djwdcmwdz/image/upload/v1672054765/Blogposts/observability-driven-development-with-go-and-tracetest/screely-1672054760024_mkzyc0.png

我们现在开始看起来像奇怪的专业人士!但是,我们还没有做到。我们想向我们的书店添加可用性检查。如果书没有库存怎么办?我们需要能够检查。

设置多个微服务的可观察性驱动测试

跟随,您可以check out the sample code we’ve prepared for part 2.2。遵循以下说明:

git clone git@github.com:kubeshop/tracetest.git
cd examples/observability-driven-development-go-tracetest/bookstore/part2.2
docker compose -f docker-compose.yaml -f tracetest/docker-compose.yaml up
###
tracetest test run -d ./e2e/books-list.yaml -w

首先将另一本书的条目添加到getBooks功能。我们将添加一项可用性检查,以确认其缺货。

// ./books/main.go

// ...

func getBooks(ctx context.Context) ([]book, error) {
    return []book{
        {"1", "Harry Potter", 0},
        {"2", "Foundation", 0},
        {"3", "Moby Dick", 0},
        {"4", "The art of war", 0}, // Add this book
    }, nil
}

再次打开books-list.yaml。让我们添加有关可用性的断言。

# books-list.yaml

# ...

  specs:
    - selector: span[name="Tracetest trigger"]
      assertions:
        - attr:tracetest.response.status = 200
    - selector: span[name="Books List"]
      assertions:
        - attr:tracetest.selected_spans.count = 1
        - attr:books.list.count = 3

    # This selector will look for a descendant of the 
    # "Books List" span called "Availability Check"
    - selector: span[name = "Books List"] span[name = "Availability Check"]
      assertions:
        - attr:tracetest.selected_spans.count = 4

我们要确保对getBooks函数的每本书执行可用性检查。

重新运行测试将导致由于预期的可用性检查而导致其失败。

tracetest test run -d ./e2e/books-list.yaml -w

[Output]
✘ Books Listing (http://localhost:11633/test/k6hEWU54R/run/2/test)
    ✔ span[name="Tracetest trigger"]#b81c6b68711908e1
            ✔ attr:tracetest.response.status = 200 (200)
    ✔ span[name="Books List"]#392fcfe7690310d8
            ✔ attr:tracetest.selected_spans.count = 1 (1)
            ✔ attr:books.list.count = 3 (3)
    ✘ span[name = "Books List"] span[name = "Availability Check"]#meta
            ✘ attr:tracetest.selected_spans.count = 4 (0) (http://localhost:11633/test/k6hEWU54R/run/2/test?selectedAssertion=2)

接下来,让我们写代码以将HTTP请求发送到availability微服务。

// ./books/main.go

// ...

func httpError(span trace.Span, w http.ResponseWriter, msg string, err error) {
    w.WriteHeader(http.StatusInternalServerError)
    io.WriteString(w, msg)
    span.RecordError(err)
    span.SetStatus(codes.Error, msg)
}

func booksListHandler(w http.ResponseWriter, r *http.Request) {
    ctx, span := tracer.Start(r.Context(), "Books List")
    defer span.End()

    books, err := getAvailableBooks(ctx)
    if err != nil {
        httpError(span, w, "cannot read books DB", err)
        return
    }

    span.SetAttributes(
        attribute.Int("books.list.count", len(books)),
    )

    jsonBooks, err := json.Marshal(books)
    if err != nil {
        httpError(span, w, "cannot json encode books", err)
        return
    }

    w.Write(jsonBooks)
}

func getAvailableBooks(ctx context.Context) ([]book, error) {
    books, err := getBooks(ctx)
    if err != nil {
        return nil, err
    }

    availableBook := make([]book, 0, len(books))
    for _, book := range books {
        available, err := isBookAvailable(ctx, book.ID)
        if err != nil {
            return nil, err
        }

        if !available {
            continue
        }
        availableBook = append(availableBook, book)
    }

    return availableBook, nil
}

var httpClient = &http.Client{
    Transport: otelhttp.NewTransport(http.DefaultTransport),
}

func isBookAvailable(ctx context.Context, bookID string) (bool, error) {
    ctx, span := tracer.Start(ctx, "Availability Request", trace.WithAttributes(
        attribute.String("bookID", bookID),
    ))
    defer span.End()

    url := "http://availability:8000/" + bookID
    req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    resp, err := httpClient.Do(req)
    if err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, "cannot do request")
        return false, err
    }

    if resp.StatusCode == http.StatusNotFound {
        span.SetStatus(codes.Error, "not found")
        return false, nil
    }

    stockBytes, err := io.ReadAll(resp.Body)
    if err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, "cannot read response body")
        return false, err
    }

    stock, err := strconv.Atoi(string(stockBytes))
    if err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, "cannot parse stock value")
        return false, err
    }

    return stock > 0, nil
}

让我详细说明代码。

  • 我们添加了一个isBookAvailable功能,该功能检查是否基于提供的bookID可用。它调用"http://availability:8000/"端点并附加一个bookID值。
  • 然后在getAvailableBooks函数中使用isBookAvailable函数,该功能通过getBooks函数的书籍进行迭代。
  • booksListHandler函数现在调用getAvailableBooks函数,而不是调用getBooks
  • httpError只是一个辅助功能。

注意:如果您更改需要下载模块的代码,请不要忘记重新运行go mod tidy。在编辑代码后,请确保还重新启动Docker撰写!

让测试重新进行。

tracetest test run -d ./e2e/books-list.yaml -w

[Output]
✘ Books Listing (http://localhost:11633/test/qasYcU54R/run/1/test)
    ✘ span[name="Tracetest trigger"]#2f9bc366597fb472
            ✘ attr:tracetest.response.status = 200 (500) (http://localhost:11633/test/qasYcU54R/run/1/test?selectedAssertion=0&selectedSpan=2f9bc366597fb472)
    ✘ span[name="Books List"]#1f0e9347869fd8c2
            ✔ attr:tracetest.selected_spans.count = 1 (1)
            ✘ attr:books.list.count = 3 (http://localhost:11633/test/qasYcU54R/run/1/test?selectedAssertion=1&selectedSpan=1f0e9347869fd8c2)
    ✘ span[name = "Books List"] span[name = "Availability Check"]#meta
            ✘ attr:tracetest.selected_spans.count = 4 (0) (http://localhost:11633/test/qasYcU54R/run/1/test?selectedAssertion=2)

我们现在遇到了不同的错误! "Tracetest trigger"跨度的响应状态等于500。嗯,不是很好,对吗?

错误!我们走上了正确的道路!该测试失败了,因为我们添加了将HTTP请求发送到不存在的可用性服务的代码。让我们修复。

接下来,创建可用服务。

跨多个服务基于痕量测试

要跟随,您可以check out the sample code we’ve prepared for part 3.1。遵循以下说明:

git clone git@github.com:kubeshop/tracetest.git
cd examples/observability-driven-development-go-tracetest/bookstore/part3.1
docker compose -f docker-compose.yaml -f tracetest/docker-compose.yaml up
###
tracetest test run -d ./e2e/books-list.yaml -w

注意:查看我们observability-driven development video tutorial, here的第三部分!

开发分布式应用程序和微服务时,将OpenTelemetry仪器提取到专用模块时是最好的做法。

这将使您将OpenTelemetry配置导入到所有微服务中,而无需重复代码。

让我们从从./books/main.go中拉出opentelemetry sdks开始,然后将它们放入一个名为instrumentation.go的专用文件中。

bookstore目录的根部创建一个lib目录。

用:
初始化一个模块

cd ./lib
go mod init github.com/your-username/bookstore/lib

注意:文件路径在GO中可能很棘手。当您导入微服务中的模块时,请确保文件路径的名称与GitHub上的位置匹配。

创建GO模块后,创建另一个名为instrumentation的目录。添加一个名为instrumentation.go的文件。

./books/main.go中删除opentelemetry仪器代码,然后将i添加到./lib/instrumentation/instrumentation.go

// ./lib/instrumentation/instrumentation.go 

package instrumentation

import (
    "context"
    "fmt"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/propagation"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func NewExporter(ctx context.Context) (sdktrace.SpanExporter, error) {
    ctx, cancel := context.WithTimeout(ctx, time.Second)
    defer cancel()
    conn, err := grpc.DialContext(ctx, "otel-collector:4317", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock())
    if err != nil {
        return nil, fmt.Errorf("failed to create gRPC connection to collector: %w", err)
    }

    traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))
    if err != nil {
        return nil, fmt.Errorf("failed to create trace exporter: %w", err)
    }

    return traceExporter, nil
}

func NewTraceProvider(svcName string, exp sdktrace.SpanExporter) *sdktrace.TracerProvider {
    // Ensure default SDK resources and the required service name are set.
    r, err := resource.Merge(
        resource.Default(),
        resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceNameKey.String(svcName),
        ),
    )

    if err != nil {
        panic(err)
    }

    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exp),
        sdktrace.WithResource(r),
    )

    otel.SetTextMapPropagator(
        propagation.NewCompositeTextMapPropagator(
            propagation.TraceContext{},
            propagation.Baggage{},
        ),
    )

    return tp
}

不要忘记从./lib文件夹中在终端中运行go mod tidy,以确保下载并保存依赖项。现在,您可以安全地提交并将此代码推向GitHub。这将使您下载并在booksavailability微服务中使用它。让我们继续首先更新books服务。

// ./books/main.go

package main

import (
    "context"
    "encoding/json"
    "io"
    "log"
    "net/http"
    "strconv"

    "github.com/gorilla/mux"

    // Add the instrumentation module from lib
    // Make sure to first push the module to GitHub
    // Watch out to get the directory tree and name to match
    "github.com/your-username/bookstore/lib/instrumentation"

    "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/codes"
    "go.opentelemetry.io/otel/trace"
)

const svcName = "books"

var tracer trace.Tracer

func main() {
    ctx := context.Background()

    // Calling the "instrumentation" module
    exp, err := instrumentation.NewExporter(ctx)
    if err != nil {
        log.Fatalf("failed to initialize exporter: %v", err)
    }

    // Calling the "instrumentation" module
    // Create a new tracer provider with a batch span processor and the given exporter.
    tp := instrumentation.NewTraceProvider(svcName, exp)

    // Handle shutdown properly so nothing leaks.
    defer func() { _ = tp.Shutdown(ctx) }()

    otel.SetTracerProvider(tp)

    // Finally, set the tracer that can be used for this package.
    tracer = tp.Tracer(svcName)

    r := mux.NewRouter()
    r.Use(otelmux.Middleware(svcName))

    r.HandleFunc("/books", booksListHandler)

    http.Handle("/", r)

    log.Fatal(http.ListenAndServe(":8001", nil))
}

// ...

./books/main.go外观完全相同,除了删除OpentElemetry仪器代码并导入./lib/instrumentation模块。确保编辑导入以使用您推到github的仪器模块。

然后,我们使用instrumentation对象调用NewExporterNewTraceProvider方法。

确保此更改后的行为是相同的

tracetest test run -d ./e2e/books-list.yaml -w

[Output]
✘ Books Listing (http://localhost:11633/test/qasYcU54R/run/1/test)
    ✘ span[name="Tracetest trigger"]#831e781a89050f81
            ✘ attr:tracetest.response.status = 200 (500) (http://localhost:11633/test/qasYcU54R/run/1/test?selectedAssertion=0&selectedSpan=831e781a89050f81)
    ✘ span[name="Books List"]#9f05d0fe6d4966e6
            ✔ attr:tracetest.selected_spans.count = 1 (1)
            ✘ attr:books.list.count = 3 (http://localhost:11633/test/qasYcU54R/run/1/test?selectedAssertion=1&selectedSpan=9f05d0fe6d4966e6)
    ✘ span[name = "Books List"] span[name = "Availability Check"]#meta
            ✘ attr:tracetest.selected_spans.count = 4 (0) (http://localhost:11633/test/qasYcU54R/run/1/test?selectedAssertion=2)

真棒!我们遇到与以前相同的问题。野,不是吗?我之所以欢呼,是因为我们遇到了与以前相同的问题!

否则,是时候构建我们​​的availability服务了。

跨多个微服务的动手可观察性驱动测试

查看sample code we’ve prepared for part 3.2以跟随。遵循以下说明:

git clone git@github.com:kubeshop/tracetest.git
cd examples/observability-driven-development-go-tracetest/bookstore/part3.2
docker compose -f docker-compose.yaml -f tracetest/docker-compose.yaml up
###
tracetest test run -d ./e2e/books-list.yaml -w

要开始,我们需要一个新的目录来进行额外的微服务。在bookstore目录中创建一个availability目录。初始化GO模块。


cd ./availability
go mod init github.com/your-username/bookstore/availability

创建一个名为./availability/main.go的文件。将此代码粘贴到其中。

package main

import (
    "context"
    "io"
    "log"
    "net/http"

    // Make sure this module matches the lib/instrumentation
    // module from the previous section!
    "github.com/your-username/bookstore/lib/instrumentation"
    "github.com/gorilla/mux"
    "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/codes"
    "go.opentelemetry.io/otel/trace"
)

const svcName = "availability"

var tracer trace.Tracer

func main() {
    ctx := context.Background()

    exp, err := instrumentation.NewExporter(ctx)
    if err != nil {
        log.Fatalf("failed to initialize exporter: %v", err)
    }

    // Create a new tracer provider with a batch span processor and the given exporter.
    tp := instrumentation.NewTraceProvider(svcName, exp)

    // Handle shutdown properly so nothing leaks.
    defer func() { _ = tp.Shutdown(ctx) }()

    otel.SetTracerProvider(tp)

    // Finally, set the tracer that can be used for this package.
    tracer = tp.Tracer(svcName)

    r := mux.NewRouter()
    r.Use(otelmux.Middleware(svcName))

    r.HandleFunc("/{bookID}", stockHandler)

    http.Handle("/", r)

    log.Fatal(http.ListenAndServe(":8000", nil))
}

var books = map[string]string{
    "1": "10",
    "2": "1",
    "3": "5",
    "4": "0",
}

func stockHandler(w http.ResponseWriter, r *http.Request) {
    _, span := tracer.Start(r.Context(), "Availability Check")
    defer span.End()

    vars := mux.Vars(r)
    bookID, ok := vars["bookID"]
    if !ok {
        span.SetStatus(codes.Error, "no bookID in URL")
        w.WriteHeader(http.StatusBadRequest)
        io.WriteString(w, "missing bookID in URL")
        return
    }

    // The span we will run an assertion against
    span.SetAttributes(
        attribute.String("bookID", bookID),
    )

    stock, ok := books[bookID]
    if !ok {
        span.SetStatus(codes.Error, "book not found")
        w.WriteHeader(http.StatusNotFound)
        io.WriteString(w, "book not found")
        return
    }

    w.WriteHeader(http.StatusOK)
    io.WriteString(w, stock)
}

一如既往,运行go mod tidy生成go.sum文件并下载模块。

让我解释代码:

  • 我们在main函数中使用的NewExporterNewTraceProvider就像在./books/main.go中一样。
  • 我们正在端口8000上运行HTTP服务器,该服务器期望bookID作为参数。
  • HTTP Route /{bookID}将触发stockHandler函数。此功能检查本书是否有库存。

很棒!添加了可用性服务,我们还需要将其添加到docker-compose.yaml中。

services:
  books:
    image: your_username/books
    build:
      args:
        SERVICE: books
    ports:
      - 8001:8001
    depends_on:
      - otel-collector

  availability:
    image: your_username/availability
    build:
      args:
        SERVICE: availability
    depends_on:
      - otel-collector

availability服务将使用与books服务相同的Dockerfile

这就是全部!我们完成了!让我们重新启动Docker组成,看看"Books Listing"测试是否通过。

供参考,这里是我们运行的完整./e2e/books-list.yaml测试文件:

# ./e2e/books-list.yaml

type: Test
spec:
  id: qasYcU54R
  name: Books Listing
  description: Try books service
  trigger:
    type: http
    httpRequest:
      url: http://books:8001/books
      method: GET
      headers:
      - key: Content-Type
        value: application/json
  specs:
    - selector: span[name="Tracetest trigger"]
      assertions:
        - attr:tracetest.response.status = 200
    - selector: span[name="Books List"]
      assertions:
        - attr:tracetest.selected_spans.count = 1
        - attr:books.list.count = 3
    - selector: span[name = "Books List"] span[name = "Availability Check"]
      assertions:
        - attr:tracetest.selected_spans.count = 4

在您的终端中,运行:

tracetest test run -d ./e2e/books-list.yaml -w

[Output]
✔ Books Listing (http://localhost:11633/test/qasYcU54R/run/1/test)

单击链接将打开Tracetest Web UI并详细显示断言。

https://res.cloudinary.com/djwdcmwdz/image/upload/v1671802096/Blogposts/observability-driven-development-with-go-and-tracetest/screely-1671802081166_b2euer.png

我们可以清楚地看到如何触发可用性检查四次。列表中的每本书一次。

结论

您学习了如何通过使用专用的OpentElemetry仪器模块在多个微服务中使用GO和Tracetest练习观察性驱动的开发。做得好!

要继续学习可观察性驱动的开发,请查看我们的3-part video tutorial关于使用GO和Tracetest的可观察性驱动的开发!

通过遵循我们的quick start guide,请尝试在您自己的应用程序中尝试并跟踪基础架构,该quick start guide可以通过几个步骤或我们的detailed guide来为您提供CLI Tooling和Tracetest Server,以获取更多详细信息。也随意到give us a star on GitHub

通过练习可观察性驱动的开发和基于痕量的测试最佳实践,我们希望您通过增加测试覆盖范围,使自己摆脱手动测试程序,并确定您不知道存在的瓶颈,从而获得更友好的开发人员体验。

我们很高兴听到您在Discord中的奇怪成功故事!我们真正重视您的反馈,所以不要害羞!