commit 414fd593f3cef665a4dd180ef7a60b53895f52a2 Author: Maksim Syomochkin Date: Thu Dec 12 08:18:01 2024 +0300 inital commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..5fe5d0a --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# Redis example +Код реазлизует простейший веб сервис отдающий информацию о пользователе по его id. +```sh +❯ http http://127.0.0.1:8080/user?id=1 +HTTP/1.1 200 OK +Content-Length: 37 +Content-Type: application/json +Date: Thu, 12 Dec 2024 05:08:45 GMT + +{ + "age": 30, + "id": 1, + "name": "Test User" +} +``` + +Данные о пользователях хранятся в PostgreSQL. Для ускорения ответа используется Redis. + +## Запуск +В папке `deployment` находится `docker-compose.yml` файл, который запускает все необходимые зависимости: +- `PostgreSQL` - основное хранилище данных +- `Redis` - кэш +- `OpenTelemetry Collector` - сборщик трейсов +- `Jaeger` - визуализация трейсов + +`go run *.go` - запуск сервиса + +После запуска сервиса следует сделать к нему ряд запросов. В интерфейсе [Jaeger](http://127.0.0.1:16686/) будет видно, что запросы которые идут подряд меньше чем за десять секунд, используют кэш и отрабатывают быстрее. \ No newline at end of file diff --git a/deployment/compose.yaml b/deployment/compose.yaml new file mode 100644 index 0000000..ebf7812 --- /dev/null +++ b/deployment/compose.yaml @@ -0,0 +1,39 @@ +version: '3.8' + +services: + postgres: + image: postgres:latest + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: mydb + ports: + - "5432:5432" + volumes: + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + + keydb: + image: eqalpha/keydb:latest + ports: + - "6379:6379" + + otel-collector: + image: otel/opentelemetry-collector:latest + container_name: otel-collector + command: ["--config=/etc/otel-collector-config.yaml"] + volumes: + - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml + ports: + - "4317:4317" + depends_on: + - jaeger + + jaeger: + image: jaegertracing/all-in-one:latest + container_name: jaeger + environment: + - COLLECTOR_OTLP_ENABLED=true + - COLLECTOR_OTLP_GRPC_HOST-PORT=:4317 + - COLLECTOR_OTLP_GRPC_HOST_PORT=:4317 + ports: + - "16686:16686" diff --git a/deployment/init.sql b/deployment/init.sql new file mode 100644 index 0000000..7e4b0f4 --- /dev/null +++ b/deployment/init.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS users ( + id INT PRIMARY KEY, + name VARCHAR(100), + age INT +); + +INSERT INTO users (id, name, age) +VALUES (1, 'Test User', 30) +ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/deployment/otel-collector-config.yaml b/deployment/otel-collector-config.yaml new file mode 100644 index 0000000..d0410d3 --- /dev/null +++ b/deployment/otel-collector-config.yaml @@ -0,0 +1,19 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 +processors: +exporters: + otlp: + endpoint: "http://jaeger:4317" + tls: + insecure: true + debug: + verbosity: detailed +service: + pipelines: + traces: + receivers: [otlp] + processors: [] + exporters: [otlp, debug] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1d4c314 --- /dev/null +++ b/go.mod @@ -0,0 +1,35 @@ +module github.com/maksim77/redis_example + +go 1.23.0 + +require ( + github.com/XSAM/otelsql v0.35.0 + github.com/lib/pq v1.10.9 + github.com/redis/go-redis/extra/redisotel/v9 v9.7.0 + github.com/redis/go-redis/v9 v9.7.0 + go.opentelemetry.io/otel v1.32.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 + go.opentelemetry.io/otel/sdk v1.32.0 +) + +require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.7.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..937e2c3 --- /dev/null +++ b/go.sum @@ -0,0 +1,71 @@ +github.com/XSAM/otelsql v0.35.0 h1:nMdbU/XLmBIB6qZF61uDqy46E0LVA4ZgF/FCNw8Had4= +github.com/XSAM/otelsql v0.35.0/go.mod h1:wO028mnLzmBpstK8XPsoeRLl/kgt417yjAwOGDIptTc= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/extra/rediscmd/v9 v9.7.0 h1:BIx9TNZH/Jsr4l1i7VVxnV0JPiwYj8qyrHyuL0fGZrk= +github.com/redis/go-redis/extra/rediscmd/v9 v9.7.0/go.mod h1:eTg/YQtGYAZD5r3DlGlJptJ45AHA+/G+2NPn30PKzik= +github.com/redis/go-redis/extra/redisotel/v9 v9.7.0 h1:bQk8xiVFw+3ln4pfELVktpWgYdFpgLLU+quwSoeIof0= +github.com/redis/go-redis/extra/redisotel/v9 v9.7.0/go.mod h1:0LyN+GHLIJmKtjYRPF7nHyTTMV6E91YngoOopNifQRo= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..b22e98e --- /dev/null +++ b/main.go @@ -0,0 +1,147 @@ +package main + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "log" + "net/http" + "strconv" + "time" + + "github.com/XSAM/otelsql" + _ "github.com/lib/pq" + "github.com/redis/go-redis/extra/redisotel/v9" + "github.com/redis/go-redis/v9" + "go.opentelemetry.io/otel" +) + +// User представляет структуру пользователя. +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Age int `json:"age"` +} + +var ( + db *sql.DB + rdb *redis.Client + tracer = otel.Tracer("redis_example") +) + +// initDB инициализирует подключение к базе данных. +func initDB() error { + var err error + connStr := "postgres://user:password@localhost/mydb?sslmode=disable" + db, err = otelsql.Open("postgres", connStr) + if err != nil { + return fmt.Errorf("ошибка подключения к БД: %v", err) + } + return db.Ping() +} + +// initRedis инициализирует подключение к Redis/KeyDB. +func initRedis() { + rdb = redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + }) + if err := redisotel.InstrumentTracing(rdb); err != nil { + panic(err) + } +} + +// getUserByID возвращает пользователя из БД по ID, с кешированием в Redis. +func getUserByID(ctx context.Context, id int) (*User, error) { + ctxLocal, span := tracer.Start(ctx, "getUserByID") + defer span.End() + + // Пытаемся получить данные из кеша. + cachedUser, err := rdb.Get(ctxLocal, strconv.Itoa(id)).Result() + if err == nil { + var user User + if err := json.Unmarshal([]byte(cachedUser), &user); err == nil { + return &user, nil + } + } + + var user User + query := "SELECT id, name, age FROM users WHERE id = $1" + err = db.QueryRowContext(ctxLocal, query, id).Scan(&user.ID, &user.Name, &user.Age) + if err == sql.ErrNoRows { + return nil, nil // пользователь не найден + } else if err != nil { + return nil, fmt.Errorf("ошибка запроса: %v", err) + } + + // Сохраняем результаты в кеш. + encodedUser, err := json.Marshal(user) + if err == nil { + rdb.Set(ctxLocal, strconv.Itoa(user.ID), encodedUser, time.Second*10) + } + + return &user, nil +} + +// userHandler обрабатывает запросы для получения пользователя. +func userHandler(w http.ResponseWriter, r *http.Request) { + ctxLocal, span := tracer.Start(r.Context(), "handler") + defer span.End() + + ids := r.URL.Query().Get("id") + if ids == "" { + http.Error(w, "id не указан", http.StatusBadRequest) + return + } + + id, err := strconv.Atoi(ids) + if err != nil { + http.Error(w, "id должен быть числом", http.StatusBadRequest) + return + } + + user, err := getUserByID(ctxLocal, id) + if err != nil { + http.Error(w, fmt.Sprintf("ошибка получения пользователя: %v", err), http.StatusInternalServerError) + return + } + + if user == nil { + http.Error(w, "пользователь не найден", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(user); err != nil { + http.Error(w, fmt.Sprintf("ошибка кодирования ответа: %v", err), http.StatusInternalServerError) + } +} + +func main() { + shutdown, err := InstallExportPipeline() + if err != nil { + log.Fatal(err.Error()) + } + defer func() { + if err := shutdown(context.Background()); err != nil { + log.Fatal(err.Error()) + } + }() + + // Инициализируем подключение к БД. + if err := initDB(); err != nil { + log.Fatalf("не удалось инициализировать БД: %v", err) + } + defer db.Close() + + // Инициализируем подключение к Redis. + initRedis() + defer rdb.Close() + + // Регистрируем обработчик. + http.HandleFunc("/user", userHandler) + + // Запускаем сервер. + fmt.Println("Сервер запущен на :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/otel.go b/otel.go new file mode 100644 index 0000000..32a48c0 --- /dev/null +++ b/otel.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "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.4.0" +) + +func InstallExportPipeline() (func(context.Context) error, error) { + traceClient := otlptracegrpc.NewClient( + otlptracegrpc.WithInsecure(), + otlptracegrpc.WithEndpoint("127.0.0.1:4317"), + ) + sctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + traceExp, err := otlptrace.New(sctx, traceClient) + if err != nil { + log.Fatal(err) + } + if err != nil { + return nil, fmt.Errorf("creating stdout exporter: %w", err) + } + + res, err := resource.New(context.Background(), + resource.WithFromEnv(), + resource.WithProcess(), + resource.WithTelemetrySDK(), + resource.WithHost(), + resource.WithAttributes( + semconv.ServiceNameKey.String("redis_example"), + ), + ) + if err != nil { + log.Fatal(err) + } + + tracerProvider := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(traceExp), + sdktrace.WithResource(res), + ) + otel.SetTracerProvider(tracerProvider) + otel.SetTextMapPropagator(propagation.TraceContext{}) + + return tracerProvider.Shutdown, nil +}