Перейти к содержимому
К программе курса
Распределенная трассировка: от основ до production
9 / 1850%

gRPC Distributed Tracing: от proto до production

70 минут

gRPC Distributed Tracing: от proto до production

Цель урока

Научиться трассировать gRPC сервисы, включая streaming calls. Вы узнаете:

  • Как gRPC metadata отличается от HTTP headers
  • Автоматическая инструментация gRPC с OpenTelemetry
  • Manual interceptors для custom spans
  • Трассировка всех типов streaming (server/client/bidirectional)
  • Debugging gRPC errors через traces
  • Production best practices для gRPC tracing

Готовые примеры кода

Полная gRPC система с тремя сервисами и всеми типами streaming доступна в репозитории.

Готовые примеры: gRPC Tracing

См. директорию: 08-grpc-tracing/ в примерах курса

Быстрый старт:

cd 08-grpc-tracing
docker-compose up -d
 
# Выполнить gRPC запрос через gateway
curl http://localhost:8080/api/user/42
 
# Посмотреть trace
open http://localhost:16686

Что включено:

  • API Gateway (Go) — gRPC client на порту 8080
  • User Service (Node.js) — gRPC server на порту 50052
  • Payment Service (Python) — gRPC server на порту 50053
  • Proto definitions для всех сервисов
  • Примеры unary, server streaming, client streaming, bidirectional streaming
  • Jaeger для визуализации

Подробные инструкции: README.md


Почему gRPC отличается от HTTP?

gRPC использует HTTP/2 и Protocol Buffers, что делает трассировку немного иначе:

Ключевые отличия:

АспектHTTP RESTgRPC
ProtocolHTTP/1.1HTTP/2
FormatJSON/XMLProtocol Buffers (binary)
Context PropagationHTTP headers (traceparent)gRPC metadata (grpc-trace-bin)
Streaming❌ Нет✅ Server/Client/Bidi
InstrumentationAuto (HTTP middleware)Auto + Manual interceptors

Хорошая новость: OpenTelemetry поддерживает gRPC из коробки! 🎉


Архитектура для практики

Создадим систему с тремя gRPC сервисами:

Что увидим в Jaeger:


Proto Definitions

Создайте файл user.proto:

syntax = "proto3";
 
package user;
 
// User Service
service UserService {
  // Unary call
  rpc FetchUser (FetchUserRequest) returns (UserResponse);
 
  // Server streaming
  rpc StreamUsers (StreamUsersRequest) returns (stream UserResponse);
 
  // Client streaming
  rpc CreateUsers (stream CreateUserRequest) returns (CreateUsersResponse);
 
  // Bidirectional streaming
  rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
 
message FetchUserRequest {
  int32 user_id = 1;
}
 
message UserResponse {
  int32 id = 1;
  string name = 2;
  string email = 3;
}
 
message StreamUsersRequest {
  int32 page_size = 1;
}
 
message CreateUserRequest {
  string name = 1;
  string email = 2;
}
 
message CreateUsersResponse {
  int32 count = 1;
}
 
message ChatMessage {
  string user = 1;
  string message = 2;
  int64 timestamp = 3;
}

Файл payment.proto:

syntax = "proto3";
 
package payment;
 
service PaymentService {
  rpc GetBalance (GetBalanceRequest) returns (BalanceResponse);
  rpc ProcessPayment (PaymentRequest) returns (PaymentResponse);
}
 
message GetBalanceRequest {
  int32 user_id = 1;
}
 
message BalanceResponse {
  double amount = 1;
  string currency = 2;
}
 
message PaymentRequest {
  int32 user_id = 1;
  double amount = 2;
}
 
message PaymentResponse {
  bool success = 1;
  string transaction_id = 2;
}

Docker Compose Setup

Создайте docker-compose.yml:

version: "3.8"
 
services:
  jaeger:
    image: jaegertracing/all-in-one:1.53
    ports:
      - "16686:16686" # UI
      - "4317:4317" # OTLP gRPC
    environment:
      - COLLECTOR_OTLP_ENABLED=true
 
  # gRPC сервисы будут запускаться локально для удобства debugging

Часть 1: Unary Calls (простые вызовы)

User Service: gRPC Server (Node.js)

Структура проекта:

user-service/
├── package.json
├── proto/
│   └── user.proto
├── tracing.js
└── server.js

Файл package.json:

{
  "name": "user-service-grpc",
  "version": "1.0.0",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "@grpc/grpc-js": "^1.9.0",
    "@grpc/proto-loader": "^0.7.0",
    "@opentelemetry/sdk-node": "^0.46.0",
    "@opentelemetry/api": "^1.7.0",
    "@opentelemetry/instrumentation-grpc": "^0.46.0",
    "@opentelemetry/exporter-trace-otlp-grpc": "^0.46.0",
    "@opentelemetry/semantic-conventions": "^1.18.1"
  }
}

Файл tracing.js:

const { NodeSDK } = require("@opentelemetry/sdk-node");
const {
  OTLPTraceExporter,
} = require("@opentelemetry/exporter-trace-otlp-grpc");
const { GrpcInstrumentation } = require("@opentelemetry/instrumentation-grpc");
 
const sdk = new NodeSDK({
  serviceName: "user-service-grpc",
  traceExporter: new OTLPTraceExporter({
    url: "localhost:4317", // gRPC endpoint
  }),
  instrumentations: [
    new GrpcInstrumentation({
      // ⭐ Автоматическая инструментация gRPC
    }),
  ],
});
 
sdk.start();
 
process.on("SIGTERM", () => {
  sdk.shutdown().finally(() => process.exit(0));
});
 
module.exports = sdk;

Файл server.js:

require("./tracing"); // ВАЖНО: Первая строка!
 
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");
const path = require("path");
const { trace, SpanStatusCode } = require("@opentelemetry/api");
 
// Загрузка proto файла
const PROTO_PATH = path.join(__dirname, "proto", "user.proto");
const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const userProto = grpc.loadPackageDefinition(packageDefinition).user;
 
const tracer = trace.getTracer("user-service-grpc");
 
// Mock database
const users = {
  1: { id: 1, name: "Alice", email: "alice@example.com" },
  2: { id: 2, name: "Bob", email: "bob@example.com" },
  42: { id: 42, name: "Charlie", email: "charlie@example.com" },
};
 
// ⭐ Unary RPC handler
function fetchUser(call, callback) {
  // ✅ Trace context автоматически извлекается из gRPC metadata!
  const userId = call.request.user_id;
 
  // Manual span для business logic
  return tracer.startActiveSpan("fetchUser.logic", (span) => {
    span.setAttribute("user.id", userId);
 
    const user = users[userId];
 
    if (!user) {
      const error = new Error(`User ${userId} not found`);
      span.recordException(error);
      span.setStatus({ code: SpanStatusCode.ERROR });
      span.end();
 
      callback({
        code: grpc.status.NOT_FOUND,
        message: error.message,
      });
      return;
    }
 
    span.setAttribute("user.name", user.name);
    span.setStatus({ code: SpanStatusCode.OK });
    span.end();
 
    callback(null, user);
  });
}
 
// Создание gRPC сервера
const server = new grpc.Server();
 
server.addService(userProto.UserService.service, {
  FetchUser: fetchUser,
  // Остальные методы добавим позже
});
 
const PORT = "50052";
server.bindAsync(
  `0.0.0.0:${PORT}`,
  grpc.ServerCredentials.createInsecure(),
  (err, port) => {
    if (err) {
      console.error("Failed to bind server:", err);
      return;
    }
    console.log(`✅ User Service (gRPC) running on port ${port}`);
    server.start();
  }
);

Ключевые моменты Node.js:

  1. GrpcInstrumentation - автоматически инструментирует gRPC calls
  2. Trace context - автоматически извлекается из gRPC metadata
  3. Manual spans - можно добавлять для business logic
  4. Error handling - span.recordException() для gRPC errors

API Gateway: gRPC Client (Python)

Структура проекта:

api-gateway/
├── requirements.txt
├── proto/
│   ├── user_pb2.py (generated)
│   └── user_pb2_grpc.py (generated)
├── tracing.py
└── client.py

Файл requirements.txt:

grpcio==1.59.0
grpcio-tools==1.59.0
opentelemetry-sdk==1.21.0
opentelemetry-exporter-otlp-proto-grpc==1.21.0
opentelemetry-instrumentation-grpc==0.42b0

Генерация Python файлов из proto:

python -m grpc_tools.protoc \
  -I./proto \
  --python_out=./proto \
  --grpc_python_out=./proto \
  ./proto/user.proto

Файл tracing.py:

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient
 
# Resource
resource = Resource.create({"service.name": "api-gateway-grpc"})
 
# TracerProvider
provider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(
    OTLPSpanExporter(endpoint="localhost:4317", insecure=True)
)
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
 
# ⭐ Автоматическая инструментация gRPC client
GrpcInstrumentorClient().instrument()

Файл client.py:

import tracing  # ВАЖНО: Первая строка!
import grpc
from proto import user_pb2, user_pb2_grpc
from opentelemetry import trace
from opentelemetry.trace import SpanKind, Status, StatusCode
 
tracer = trace.get_tracer("api-gateway-grpc")
 
def get_user_profile(user_id):
    """Вызов gRPC сервиса с автоматической трассировкой"""
 
    with tracer.start_as_current_span("getUserProfile", kind=SpanKind.CLIENT):
        # Создание gRPC channel
        channel = grpc.insecure_channel("localhost:50052")
        stub = user_pb2_grpc.UserServiceStub(channel)
 
        try:
            # ⭐ gRPC call - trace context автоматически передается!
            response = stub.FetchUser(
                user_pb2.FetchUserRequest(user_id=user_id)
            )
 
            print(f"✅ Received user: {response.name} ({response.email})")
            return response
 
        except grpc.RpcError as e:
            print(f"❌ gRPC Error: {e.code()} - {e.details()}")
 
            # Записываем ошибку в span
            span = trace.get_current_span()
            span.record_exception(e)
            span.set_status(Status(StatusCode.ERROR, str(e)))
            raise
 
        finally:
            channel.close()
 
if __name__ == "__main__":
    # Тестовый вызов
    get_user_profile(42)
 
    import time
    time.sleep(2)  # Ждем отправки spans

Ключевые моменты Python:

  1. GrpcInstrumentorClient() - инструментирует gRPC client calls
  2. Trace context - автоматически передается через gRPC metadata
  3. Error handling - grpc.RpcError с трассировкой

Payment Service: gRPC Server (Go)

Структура проекта:

payment-service/
├── go.mod
├── proto/
│   ├── payment.proto
│   └── payment.pb.go (generated)
├── tracing.go
└── main.go

Файл go.mod:

module payment-service
 
go 1.21
 
require (
    google.golang.org/grpc v1.59.0
    google.golang.org/protobuf v1.31.0
    go.opentelemetry.io/otel v1.21.0
    go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0
    go.opentelemetry.io/otel/sdk v1.21.0
    go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0
)

Генерация Go файлов:

protoc --go_out=. --go-grpc_out=. proto/payment.proto

Файл tracing.go:

package main
 
import (
    "context"
    "log"
 
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)
 
func InitTracing() func() {
    ctx := context.Background()
 
    exporter, err := otlptracegrpc.New(ctx,
        otlptracegrpc.WithInsecure(),
        otlptracegrpc.WithEndpoint("localhost:4317"),
    )
    if err != nil {
        log.Fatal(err)
    }
 
    res, err := resource.New(ctx,
        resource.WithAttributes(
            semconv.ServiceName("payment-service-grpc"),
        ),
    )
    if err != nil {
        log.Fatal(err)
    }
 
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(res),
    )
 
    otel.SetTracerProvider(tp)
 
    return func() {
        if err := tp.Shutdown(ctx); err != nil {
            log.Fatal(err)
        }
    }
}

Файл main.go:

package main
 
import (
    "context"
    "log"
    "net"
 
    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/codes as traceCodes"
 
    pb "payment-service/proto"
)
 
type paymentServer struct {
    pb.UnimplementedPaymentServiceServer
}
 
// ⭐ Unary RPC handler
func (s *paymentServer) GetBalance(
    ctx context.Context,
    req *pb.GetBalanceRequest,
) (*pb.BalanceResponse, error) {
    tracer := otel.Tracer("payment-service-grpc")
 
    // Manual span для business logic
    _, span := tracer.Start(ctx, "getBalance.logic")
    defer span.End()
 
    userID := req.GetUserId()
    span.SetAttributes(attribute.Int("user.id", int(userID)))
 
    // Mock balance lookup
    balances := map[int32]float64{
        1:  1000.50,
        2:  500.00,
        42: 1500.75,
    }
 
    balance, ok := balances[userID]
    if !ok {
        err := status.Errorf(codes.NotFound, "balance for user %d not found", userID)
        span.RecordError(err)
        span.SetStatus(traceCodes.Error, err.Error())
        return nil, err
    }
 
    span.SetAttributes(attribute.Float64("balance.amount", balance))
    span.SetStatus(traceCodes.Ok, "balance retrieved")
 
    return &pb.BalanceResponse{
        Amount:   balance,
        Currency: "USD",
    }, nil
}
 
func main() {
    // Инициализация трейсинга
    shutdown := InitTracing()
    defer shutdown()
 
    lis, err := net.Listen("tcp", ":50053")
    if err != nil {
        log.Fatalf("Failed to listen: %v", err)
    }
 
    // ⭐ Создание gRPC server с OpenTelemetry interceptor
    server := grpc.NewServer(
        grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
        grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()),
    )
 
    pb.RegisterPaymentServiceServer(server, &paymentServer{})
 
    log.Println("✅ Payment Service (gRPC) running on port 50053")
    if err := server.Serve(lis); err != nil {
        log.Fatalf("Failed to serve: %v", err)
    }
}

Ключевые моменты Go:

  1. otelgrpc interceptors - автоматическая инструментация
  2. grpc.UnaryInterceptor() - для unary calls
  3. grpc.StreamInterceptor() - для streaming (добавим позже)
  4. Manual spans - tracer.Start(ctx, "name") для business logic

Часть 2: Streaming Calls

Server Streaming (многие ответы на один запрос)

Use case: Получить список всех пользователей постранично

Node.js Server (User Service):

// Добавьте в server.js
 
function streamUsers(call) {
  return tracer.startActiveSpan("streamUsers.logic", (span) => {
    const pageSize = call.request.page_size || 10;
    span.setAttribute("page.size", pageSize);
 
    // Имитация потоковой отправки
    let sent = 0;
    for (const [id, user] of Object.entries(users)) {
      call.write(user); // ⭐ Отправляем каждого пользователя отдельно
      sent++;
 
      if (sent >= pageSize) break;
    }
 
    span.setAttribute("users.sent", sent);
    span.setStatus({ code: SpanStatusCode.OK });
    span.end();
 
    call.end(); // Завершаем stream
  });
}
 
// Обновите server.addService:
server.addService(userProto.UserService.service, {
  FetchUser: fetchUser,
  StreamUsers: streamUsers, // ⭐ Добавили streaming
});

Python Client:

def stream_users(page_size=10):
    """Получение пользователей через server streaming"""
 
    with tracer.start_as_current_span("streamUsers", kind=SpanKind.CLIENT):
        channel = grpc.insecure_channel("localhost:50052")
        stub = user_pb2_grpc.UserServiceStub(channel)
 
        try:
            # ⭐ Server streaming - получаем итератор
            response_stream = stub.StreamUsers(
                user_pb2.StreamUsersRequest(page_size=page_size)
            )
 
            users = []
            for user in response_stream:  # ⭐ Итерируемся по stream
                print(f"📥 Received user: {user.name}")
                users.append(user)
 
            print(f"✅ Total users received: {len(users)}")
            return users
 
        finally:
            channel.close()

Trace в Jaeger:


Client Streaming (много запросов, один ответ)

Use case: Batch создание пользователей

Node.js Server:

function createUsers(call, callback) {
  return tracer.startActiveSpan("createUsers.logic", (span) => {
    let count = 0;
 
    call.on("data", (request) => {
      // ⭐ Получаем каждого пользователя из stream
      const user = {
        id: Object.keys(users).length + count + 1,
        name: request.name,
        email: request.email,
      };
      users[user.id] = user;
      count++;
 
      console.log(`✅ Created user: ${user.name}`);
    });
 
    call.on("end", () => {
      span.setAttribute("users.created", count);
      span.setStatus({ code: SpanStatusCode.OK });
      span.end();
 
      // ⭐ Отправляем ОДИН ответ в конце
      callback(null, { count });
    });
 
    call.on("error", (err) => {
      span.recordException(err);
      span.setStatus({ code: SpanStatusCode.ERROR });
      span.end();
      callback(err);
    });
  });
}

Python Client:

def create_users_batch(users_data):
    """Создание пользователей через client streaming"""
 
    with tracer.start_as_current_span("createUsersBatch", kind=SpanKind.CLIENT):
        channel = grpc.insecure_channel("localhost:50052")
        stub = user_pb2_grpc.UserServiceStub(channel)
 
        try:
            # ⭐ Client streaming - передаем итератор
            def request_generator():
                for user in users_data:
                    yield user_pb2.CreateUserRequest(
                        name=user["name"],
                        email=user["email"]
                    )
 
            response = stub.CreateUsers(request_generator())
            print(f"✅ Created {response.count} users")
            return response.count
 
        finally:
            channel.close()
 
# Использование:
create_users_batch([
    {"name": "Dave", "email": "dave@example.com"},
    {"name": "Eve", "email": "eve@example.com"},
])

Bidirectional Streaming (чат)

Use case: Real-time chat

Node.js Server:

function chat(call) {
  return tracer.startActiveSpan("chat.session", (span) => {
    console.log("💬 Chat session started");
 
    call.on("data", (message) => {
      // ⭐ Получаем сообщение от клиента
      console.log(`📥 ${message.user}: ${message.message}`);
 
      // Echo back (в реальности - broadcast всем)
      call.write({
        user: "Server",
        message: `Echo: ${message.message}`,
        timestamp: Date.now(),
      });
    });
 
    call.on("end", () => {
      console.log("💬 Chat session ended");
      span.setStatus({ code: SpanStatusCode.OK });
      span.end();
      call.end();
    });
 
    call.on("error", (err) => {
      span.recordException(err);
      span.setStatus({ code: SpanStatusCode.ERROR });
      span.end();
    });
  });
}

Python Client:

import threading
 
def chat_client():
    """Bidirectional streaming chat"""
 
    with tracer.start_as_current_span("chatClient", kind=SpanKind.CLIENT):
        channel = grpc.insecure_channel("localhost:50052")
        stub = user_pb2_grpc.UserServiceStub(channel)
 
        # ⭐ Bidi streaming
        def request_generator():
            messages = [
                "Hello!",
                "How are you?",
                "Bye!",
            ]
            for msg in messages:
                yield user_pb2.ChatMessage(
                    user="Alice",
                    message=msg,
                    timestamp=int(time.time() * 1000)
                )
                time.sleep(1)
 
        try:
            response_stream = stub.Chat(request_generator())
 
            # Читаем ответы
            for response in response_stream:
                print(f"📥 {response.user}: {response.message}")
 
        finally:
            channel.close()

Trace для Bidi Streaming:


Metadata Propagation (под капотом)

Как trace context передается через gRPC:

В gRPC metadata:

// Автоматически добавляется OpenTelemetry:
{
  "grpc-trace-bin": "<binary encoded trace context>",
  "traceparent": "00-abc123-def456-01",  // W3C format (опционально)
}

Если нужен manual propagation:

const { propagation, context } = require("@opentelemetry/api");
 
// В client:
const metadata = new grpc.Metadata();
propagation.inject(context.active(), metadata);
 
// В server:
const ctx = propagation.extract(context.active(), call.metadata);

Но обычно OpenTelemetry делает это автоматически! ✅


Debugging gRPC Errors

Типы gRPC ошибок

gRPC Status CodeЗначениеПример
OK (0)УспехВсе хорошо
CANCELLED (1)Отменен клиентомTimeout
NOT_FOUND (5)Не найденоUser не существует
ALREADY_EXISTS (6)Уже существуетEmail занят
PERMISSION_DENIED (7)Нет правUnauthorized
RESOURCE_EXHAUSTED (8)Лимит превышенRate limit
UNAVAILABLE (14)Сервис недоступенNetwork error
DEADLINE_EXCEEDED (4)TimeoutSlow query

Как ошибки выглядят в Jaeger

Span attributes для ошибок:

span.setAttributes({
  "rpc.system": "grpc",
  "rpc.service": "UserService",
  "rpc.method": "FetchUser",
  "rpc.grpc.status_code": 5, // NOT_FOUND
  "error.type": "NotFoundError",
  "error.message": "User 999 not found",
});

Production Best Practices

1. Deadlines и Timeouts

// Go client с deadline
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
 
response, err := stub.GetBalance(ctx, &pb.GetBalanceRequest{UserId: 42})
# Python client с timeout
response = stub.FetchUser(
    user_pb2.FetchUserRequest(user_id=42),
    timeout=2.0  # 2 seconds
)

В Jaeger увидите:

  • Если timeout: status_code: DEADLINE_EXCEEDED
  • Span длительность = ровно timeout значение

2. Health Checks - НЕ трассируем!

const { GrpcInstrumentation } = require("@opentelemetry/instrumentation-grpc");
 
new GrpcInstrumentation({
  ignoreGrpcMethods: [
    // ⭐ Игнорируем health checks
    "grpc.health.v1.Health/Check",
    "grpc.health.v1.Health/Watch",
  ],
});

3. Load Balancing Impact

Load balancer не ломает trace context - он передается через metadata!

4. TLS/mTLS Considerations

// Secure gRPC client
const credentials = grpc.credentials.createSsl(
  fs.readFileSync("ca.pem"),
  fs.readFileSync("client-key.pem"),
  fs.readFileSync("client-cert.pem")
);
 
const channel = new grpc.Channel("server:50051", credentials);

Трассировка работает одинаково с TLS и без него! ✅

5. Connection Pooling

// Переиспользуем connection для performance
var conn *grpc.ClientConn
 
func getConnection() *grpc.ClientConn {
    if conn == nil {
        conn, _ = grpc.Dial(
            "localhost:50052",
            grpc.WithInsecure(),
            grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
        )
    }
    return conn
}

Trace context передается в каждом вызове, не в connection!


Практические задания

Задание 1: Сквозной trace через 3 сервиса

  1. Запустите все 3 gRPC сервиса (User, Payment, Gateway)
  2. Вызовите GetUserProfile из Gateway
  3. Gateway должен вызвать User Service И Payment Service
  4. В Jaeger найдите trace со всеми 3 сервисами
  5. Проверьте что все spans связаны одним trace ID

Задание 2: Debugging Streaming

  1. Реализуйте Server Streaming для получения списка пользователей
  2. Вызовите с page_size=100
  3. В Jaeger проверьте:
    • Сколько времени занял stream?
    • Видны ли отдельные spans для каждого пользователя?

Задание 3: Timeout Simulation

  1. Добавьте time.sleep(3) в Payment Service
  2. Установите timeout 2s на клиенте
  3. Вызовите GetBalance
  4. В Jaeger найдите:
    • Span с DEADLINE_EXCEEDED status
    • Duration = ровно 2s (timeout)

Что дальше

В следующем уроке рассмотрим Service Mesh (Istio) - автоматическую трассировку БЕЗ изменения кода через sidecar proxies!

Поздравляем! Вы овладели gRPC трассировкой - включая все типы streaming! Теперь вы можете отлаживать даже самые сложные gRPC системы.


Дополнительные материалы