我们进入了observability-driven development的新时代。 ODD使用OpentElemetry仪器作为测试中的断言!
Here's在Twitter上的一个很棒的解释!
这正在推动trace-based testing的新文化。通过基于痕量的测试,您可以从基于OpentElemetry的痕迹中生成集成测试,执行质量,鼓励速度并增加微服务和分布式应用程序的测试覆盖范围。
今天,您将学习使用GO和Docker构建分布式系统。您可以使用opentelemetry traces进行仪器,然后使用TraceTest在OpenTelemetry基础结构之上运行基于跟踪的测试。
我们将遵循可观察性驱动的发展原则,并展示为什么在当今云中开发分布式系统的世界中,为什么它强大。
在本教程结束时,您将学习以观察性驱动的开发,如何使用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。
这是我们在本指南中使用的三个组件:
- Abiaoqian 9用于生成和发射遥测
- OpenTelemetry Collector接收,处理和导出遥测数据
- OTLP protocol用于传输遥测数据
因为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/
上打开它。
创建一个新的HTTP测试。给它一个名字,并确保将URL设置为http://books:8001/books
。
单击创建。这将触发测试立即运行。
测试将返回200
状态代码。接下来,我们需要添加针对跟踪数据的断言,以确保我们的opentelemetry跟踪仪器在我们的GO代码中起作用。
打开Trace
选项卡,让我们从添加状态代码断言开始。单击Tracetest trigger
跨度。在左导航中,选择tracetest.response.status
,然后单击Create test spec
。如果您手工编写断言,请确保在选择要断言的属性时,用attr:
将属性启动。
保存测试规范,并向Books list
跨度添加另一个断言。这次添加称为attr:tracetest.selected_spans.count = 1
的属性。
保存并发布测试规格。重新运行测试。
您现在有通过测试来确保服务使用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。
我们现在开始看起来像奇怪的专业人士!但是,我们还没有做到。我们想向我们的书店添加可用性检查。如果书没有库存怎么办?我们需要能够检查。
设置多个微服务的可观察性驱动测试
跟随,您可以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。这将使您下载并在books
和availability
微服务中使用它。让我们继续首先更新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
对象调用NewExporter
和NewTraceProvider
方法。
确保此更改后的行为是相同的
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
函数中使用的NewExporter
和NewTraceProvider
就像在./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并详细显示断言。
我们可以清楚地看到如何触发可用性检查四次。列表中的每本书一次。
结论
您学习了如何通过使用专用的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中的奇怪成功故事!我们真正重视您的反馈,所以不要害羞!