├── .env ├── .gitignore ├── models ├── .env ├── author_test.go ├── book_test.go ├── author.go └── book.go ├── goapi-v1.png ├── Dockerfile.db ├── utils ├── helper.go ├── db.go └── log.go ├── Makefile ├── book_go.sql ├── nginx └── nginx.conf ├── .github ├── release.yaml └── workflows │ └── app-ci.yaml ├── prometheus └── prometheus.go ├── Dockerfile ├── go.mod ├── docker-compose.yaml ├── main.go ├── controllers ├── author_test.go ├── book_test.go ├── book.go └── author.go ├── readme.md ├── go.sum └── logs └── api.log /.env: -------------------------------------------------------------------------------- 1 | WORK_MODE=DEV -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | prometheus-server/ -------------------------------------------------------------------------------- /models/.env: -------------------------------------------------------------------------------- 1 | WORK_MODE=TEST -------------------------------------------------------------------------------- /goapi-v1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aymane-belassiria/Booky/HEAD/goapi-v1.png -------------------------------------------------------------------------------- /Dockerfile.db: -------------------------------------------------------------------------------- 1 | FROM postgres:latest 2 | 3 | WORKDIR /db 4 | 5 | COPY ./book_go.sql . 6 | 7 | ENV POSTGRES_USER="aymane" 8 | ENV POSTGRES_PASSWORD="aymane@123" 9 | ENV POSTGRES_DB="book_go" 10 | 11 | EXPOSE 5432 -------------------------------------------------------------------------------- /utils/helper.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "net/http" 4 | 5 | func JsonWriter(w http.ResponseWriter, status int, payload []byte) { 6 | w.Header().Set("Content-Type", "application/json") 7 | w.WriteHeader(status) 8 | _, _ = w.Write(payload) 9 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run_compose_build: 2 | sudo docker-compose up -d --build 3 | run_compose: 4 | sudo docker-compose up -d --scale instance1=2 5 | down_compose: 6 | sudo docker-compose down 7 | test_server: 8 | go run main.go 9 | build_binary: 10 | go build -o booky && ./book -------------------------------------------------------------------------------- /book_go.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE authors( 2 | id SERIAL PRIMARY KEY, 3 | full_name TEXT NOT NULL 4 | ); 5 | 6 | CREATE TABLE books( 7 | id TEXT PRIMARY KEY, 8 | ISBF TEXT NOT NULL, 9 | title TEXT NOT NULL, 10 | page INT NOT NULL, 11 | author_id INT, 12 | FOREIGN KEY(author_id) REFERENCES authors(id) 13 | ); -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1000; 3 | } 4 | 5 | http { 6 | # upstream go_api_backend { 7 | # server instance1:8001; 8 | # server instance2:8002; 9 | # } 10 | server { 11 | listen 8000; 12 | 13 | server_name localhost; 14 | 15 | location / { 16 | proxy_pass http://instance1:8000; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/release.yaml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | categories: 6 | - title: Breaking Changes 🛠 7 | labels: 8 | - breaking-change 9 | - title: New Features 🎉 10 | labels: 11 | - enhancement 12 | - title: Bug Fixes 🐛 13 | labels: 14 | - bug-fix 15 | - title: Other Changes 16 | labels: 17 | - "*" -------------------------------------------------------------------------------- /utils/db.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | _ "github.com/lib/pq" 10 | ) 11 | 12 | /* 13 | using singleton design pattern for database connection 14 | */ 15 | 16 | var db *sql.DB 17 | 18 | func GetInstance() *sql.DB{ 19 | 20 | if db == nil{ 21 | connStr := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", os.Getenv("DB_USER"), os.Getenv("DB_PASS"), os.Getenv("DB_HOST"), os.Getenv("DB_NAME")) 22 | db, err := sql.Open("postgres", connStr) 23 | if err != nil{ 24 | log.Fatalf("%v", err) 25 | } 26 | return db 27 | } 28 | return db 29 | } -------------------------------------------------------------------------------- /prometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | package prometheus_book 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | var TotalRequest = prometheus.NewCounter( 6 | prometheus.CounterOpts{ 7 | Name: "book_request_count", 8 | Help: "No of request handled by the handler", 9 | }, 10 | ) 11 | 12 | var TotalErros = prometheus.NewCounterVec( 13 | prometheus.CounterOpts{ 14 | Name: "book_error_count", 15 | Help: "No of errors that raised during execution", 16 | }, 17 | []string{"type", "method", "package"}, 18 | ) 19 | 20 | var RequestDuration = prometheus.NewHistogram( 21 | prometheus.HistogramOpts{ 22 | Namespace: "request", 23 | Subsystem: "book", 24 | Name: "http_request_duration_seconds", 25 | Help: "HTTP request duration distribution", 26 | Buckets: []float64{0.1, 0.5, 1, 2, 5, 10}, 27 | }, 28 | ) -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Go runtime as the base image 2 | FROM golang:1.19 AS build 3 | 4 | # Set the working directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy go.mod and go.sum files to the container 8 | COPY go.mod go.sum ./ 9 | 10 | # Download and cache Go modules 11 | RUN go mod download 12 | 13 | # Copy the rest of the application code 14 | COPY . . 15 | 16 | RUN cd utils && ls -la 17 | 18 | # Generate env variable for db credentiels we going to add default value just in case 19 | 20 | ENV DB_USER=root 21 | 22 | ENV DB_PASS=root 23 | 24 | ENV DB_HOST=localhost 25 | 26 | ENV DB_NAME=booky 27 | 28 | #defaul port PG 29 | ENV DB_PORT=5432 30 | 31 | # Build the application 32 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /booky 33 | 34 | RUN ls -la 35 | 36 | #Expose teh default port of the app 37 | 38 | EXPOSE 8000 39 | 40 | # Run the application 41 | CMD ["/booky"] 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aymane-smi/api-test 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/google/uuid v1.3.0 7 | github.com/gorilla/mux v1.8.0 8 | github.com/lib/pq v1.10.9 9 | go.uber.org/zap v1.25.0 10 | ) 11 | 12 | require ( 13 | github.com/beorn7/perks v1.0.1 // indirect 14 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/golang/protobuf v1.5.3 // indirect 17 | github.com/joho/godotenv v1.5.1 // indirect 18 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 19 | github.com/prometheus/client_golang v1.16.0 // indirect 20 | github.com/prometheus/client_model v0.3.0 // indirect 21 | github.com/prometheus/common v0.42.0 // indirect 22 | github.com/prometheus/procfs v0.10.1 // indirect 23 | golang.org/x/sys v0.8.0 // indirect 24 | google.golang.org/protobuf v1.30.0 // indirect 25 | ) 26 | 27 | require ( 28 | github.com/steinfletcher/apitest v1.5.15 29 | go.uber.org/multierr v1.10.0 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | # grafana: 5 | # image: grafana/grafana:latest 6 | # container_name: grafana 7 | # ports: 8 | # - "3000:3000" 9 | instance1: 10 | image: aymanebel/booky:latest 11 | #container_name: instance1 12 | environment: 13 | - DB_USER=aymane 14 | - DB_PASS=aymane@123 15 | - DB_NAME=book_go 16 | - DB_HOST=postgres 17 | networks: 18 | - load 19 | # instance2: 20 | # image: aymanebel/booky:latest 21 | # container_name: instance2 22 | # environment: 23 | # - DB_USER=aymane 24 | # - DB_PASS=aymane@123 25 | # - DB_NAME=book_go 26 | # - DB_HOST=postgres 27 | # ports: 28 | # - 8002:8000 29 | # networks: 30 | # - load 31 | nginx: 32 | container_name: nginx 33 | image: nginx 34 | ports: 35 | - "8000:8000" 36 | volumes: 37 | - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro 38 | networks: 39 | - load 40 | networks: 41 | load: -------------------------------------------------------------------------------- /models/author_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aymane-smi/api-test/utils" 7 | ) 8 | 9 | func TestAddAuthor(t *testing.T){ 10 | utils.InitLogger() 11 | tmp := Author{ 12 | Full_name: "test test", 13 | } 14 | message, err := AddAuthor(tmp) 15 | 16 | if message == "" && err != nil { 17 | t.Errorf("%v", err) 18 | } 19 | } 20 | 21 | func TestAddAuthorTesting(t *testing.T){ 22 | utils.InitLogger() 23 | tmp := Author{ 24 | Full_name: "test test", 25 | } 26 | message, err := AddAuthor(tmp) 27 | 28 | if message == "" && err != nil { 29 | t.Errorf("%v", err) 30 | } 31 | } 32 | 33 | func TestUpdateAuthor(t *testing.T){ 34 | tmp := Author{ 35 | Id: 1, 36 | Full_name: "test* test*", 37 | } 38 | 39 | if author, err := UpdateAuthor(tmp); author == nil && err != nil{ 40 | t.Errorf("%v", err) 41 | } 42 | } 43 | 44 | func TestGetAuthorById(t *testing.T){ 45 | if author := GetAuthorById(1); author == nil{ 46 | t.Errorf("invalid id") 47 | } 48 | } 49 | 50 | func TestDeleteAuthorById(t *testing.T){ 51 | if msg, err := DeleteAuthorById(2); msg == "" && err != nil{ 52 | t.Errorf("%v", err) 53 | } 54 | } -------------------------------------------------------------------------------- /utils/log.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/joho/godotenv" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | var Log *zap.Logger 13 | 14 | func InitLogger(){ 15 | if err := godotenv.Load(".env"); err != nil { 16 | fmt.Println("Error loading .env file") 17 | } 18 | 19 | var path string 20 | 21 | if os.Getenv("WORK_MODE") == "TEST"{ 22 | path = "../logs/api.log" 23 | }else if os.Getenv("WORK_MODE") == "DEV"{ 24 | path = "./logs/api.log" 25 | } 26 | rawJSON := []byte(fmt.Sprintf(`{ 27 | "level": "debug", 28 | "encoding": "json", 29 | "outputPaths": ["%s"], 30 | "errorOutputPaths": ["%s"], 31 | "initialFields": {"foo": "bar"}, 32 | "encoderConfig": { 33 | "timeKey": "logged at", 34 | "timeEncoder": "ISO8601", 35 | "messageKey": "message", 36 | "levelKey": "level", 37 | "levelEncoder": "lowercase" 38 | } 39 | }`, path, path)) 40 | 41 | var cfg zap.Config 42 | if err := json.Unmarshal(rawJSON, &cfg); err != nil { 43 | panic(err) 44 | } 45 | logger := zap.Must(cfg.Build()) 46 | defer func(){ 47 | err := logger.Sync() 48 | if err != nil{ 49 | panic(err) 50 | } 51 | }() 52 | 53 | logger.Info("logger construction succeeded") 54 | 55 | Log = logger 56 | } -------------------------------------------------------------------------------- /models/book_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aymane-smi/api-test/utils" 7 | ) 8 | 9 | func TestAddBook(t *testing.T){ 10 | utils.InitLogger() 11 | tmp := Book{ 12 | ISBF: "test", 13 | Title: "test title", 14 | Page: 24, 15 | Author: 1, 16 | } 17 | message, err := AddBook(tmp) 18 | 19 | if message == "" && err != nil { 20 | t.Errorf("%v", err) 21 | } 22 | } 23 | 24 | func TestAddBookTesting(t *testing.T){ 25 | utils.InitLogger() 26 | tmp := Book{ 27 | ISBF: "test", 28 | Title: "test title", 29 | Page: 24, 30 | Author: 1, 31 | } 32 | message, err := AddBookTest(tmp) 33 | 34 | if message == "" && err != nil { 35 | t.Errorf("%v", err) 36 | } 37 | } 38 | 39 | func TestUpdateBook(t *testing.T){ 40 | utils.InitLogger() 41 | book := GetBookById("test_id") 42 | 43 | if book == nil{ 44 | t.Errorf("invalid book in the records") 45 | }else{ 46 | book.Title = "test*" 47 | book.ISBF = "test*" 48 | book.Page = 101 49 | 50 | if book, err := UpdateBook(*book); book == nil && err != nil{ 51 | t.Errorf("%v",err) 52 | } 53 | } 54 | } 55 | 56 | func TestGetById(t *testing.T){ 57 | utils.InitLogger() 58 | book := GetBookById("test_id") 59 | if book == nil { 60 | t.Errorf("error bookis empty"); 61 | } 62 | } 63 | 64 | func TestDeleteById(t *testing.T){ 65 | if msg, err := DeleteById("test_id"); msg == "" && err != nil{ 66 | t.Errorf("error while deleting the book") 67 | } 68 | } -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/aymane-smi/api-test/controllers" 8 | prometheus_book "github.com/aymane-smi/api-test/prometheus" 9 | "github.com/aymane-smi/api-test/utils" 10 | "github.com/gorilla/mux" 11 | "github.com/prometheus/client_golang/prometheus" 12 | "github.com/prometheus/client_golang/prometheus/promhttp" 13 | ) 14 | 15 | func main(){ 16 | 17 | r := mux.NewRouter() 18 | 19 | utils.InitLogger() 20 | 21 | //book routes 22 | 23 | r.HandleFunc("/book/{id}", controllers.GetBookById).Methods("GET") 24 | 25 | r.HandleFunc("/book", controllers.AddBook).Methods("POST") 26 | 27 | r.HandleFunc("/book", controllers.UpdateBook).Methods("PUT") 28 | 29 | r.HandleFunc("/book/{id}", controllers.DeleteBook).Methods("DELETE") 30 | 31 | //athor routes 32 | 33 | r.HandleFunc("/author/{id}", controllers.GetAuthorById).Methods("GET") 34 | 35 | r.HandleFunc("/author", controllers.AddAuthor).Methods("POST") 36 | 37 | r.HandleFunc("/author", controllers.UpdateAuthor).Methods("PUT") 38 | 39 | r.HandleFunc("/author/{id}", controllers.DeleteAuthor).Methods("DELETE") 40 | 41 | //prometheus route & config 42 | 43 | r.Handle("/metrics", promhttp.Handler()) 44 | 45 | prometheus.MustRegister(prometheus_book.TotalRequest) 46 | prometheus.MustRegister(prometheus_book.TotalErros) 47 | prometheus.MustRegister(prometheus_book.RequestDuration) 48 | 49 | 50 | 51 | fmt.Println("Start listening to server at port 8000") 52 | 53 | _ = http.ListenAndServe(":8000", r) 54 | 55 | } -------------------------------------------------------------------------------- /controllers/author_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/aymane-smi/api-test/models" 10 | "github.com/aymane-smi/api-test/utils" 11 | ) 12 | 13 | func TestGetAuthorById(t *testing.T){ 14 | 15 | utils.InitLogger() 16 | 17 | req, _ := http.Get("http://localhost:8000/author/4") 18 | 19 | if req.StatusCode != 200{ 20 | t.Errorf("invalid request!") 21 | } 22 | } 23 | 24 | func TestAddAuthor(t *testing.T){ 25 | 26 | utils.InitLogger() 27 | 28 | author := models.Author{ 29 | Full_name: "test author", 30 | } 31 | 32 | json, _ := json.Marshal(author) 33 | 34 | req, _ := http.Post("http://localhost:8000/author", "application/json", bytes.NewBuffer(json)) 35 | 36 | if req.StatusCode != 200{ 37 | t.Errorf("invalid request!") 38 | } 39 | } 40 | 41 | func TestUpdateAuthor(t *testing.T){ 42 | utils.InitLogger() 43 | 44 | author := models.Author{ 45 | Id: 8, 46 | Full_name: "test author*", 47 | } 48 | 49 | json, _ := json.Marshal(author) 50 | 51 | req, _ := http.NewRequest(http.MethodPut, "http://localhost:8000/author", bytes.NewBuffer(json)) 52 | 53 | req.Header.Set("Content-Type", "applicatio/json") 54 | 55 | client := &http.Client{} 56 | 57 | resp, _ := client.Do(req) 58 | 59 | if resp.StatusCode != 200{ 60 | t.Errorf("invalid request!") 61 | } 62 | } 63 | 64 | func TestDeleteAuthor(t *testing.T){ 65 | utils.InitLogger() 66 | 67 | req, _ := http.NewRequest(http.MethodDelete, "http://localhost:8000/author/8", nil) 68 | 69 | client := &http.Client{} 70 | 71 | resp, _ := client.Do(req) 72 | 73 | if resp.StatusCode != 200{ 74 | t.Errorf("invalid request!") 75 | } 76 | 77 | 78 | 79 | } -------------------------------------------------------------------------------- /controllers/book_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/aymane-smi/api-test/models" 10 | "github.com/aymane-smi/api-test/utils" 11 | ) 12 | 13 | func TestGetBookById(t *testing.T){ 14 | 15 | utils.InitLogger() 16 | 17 | req, _ := http.Get("http://localhost:8000/book/21a1e439-baf5-400a-b515-c10ed7ead9b5") 18 | 19 | if req.StatusCode != 200{ 20 | t.Errorf("invalid request!") 21 | } 22 | } 23 | 24 | func TestAddBook(t *testing.T){ 25 | 26 | utils.InitLogger() 27 | 28 | book := models.Book{ 29 | Title: "test book", 30 | ISBF: "TEST ISBF", 31 | Page: 100, 32 | Author: 4, 33 | } 34 | 35 | json, _ := json.Marshal(book) 36 | 37 | req, _ := http.Post("http://localhost:8000/book", "application/json", bytes.NewBuffer(json)) 38 | 39 | if req.StatusCode != 200{ 40 | t.Errorf("invalid request!") 41 | } 42 | } 43 | 44 | func TestUpdateBook(t *testing.T){ 45 | utils.InitLogger() 46 | 47 | book := models.Book{ 48 | Id: "2d1a8ebd-9d9c-4122-87f6-d5e305d6e46e", 49 | Title: "test book**", 50 | ISBF: "TEST ISBF**", 51 | Page: 102, 52 | Author: 5, 53 | } 54 | 55 | json, _ := json.Marshal(book) 56 | 57 | req, _ := http.NewRequest(http.MethodPut, "http://localhost:8000/book", bytes.NewBuffer(json)) 58 | 59 | req.Header.Set("Content-Type", "applicatio/json") 60 | 61 | client := &http.Client{} 62 | 63 | resp, _ := client.Do(req) 64 | 65 | if resp.StatusCode != 200{ 66 | t.Errorf("invalid request!") 67 | } 68 | } 69 | 70 | func TestDeleteBook(t *testing.T){ 71 | utils.InitLogger() 72 | 73 | req, _ := http.NewRequest(http.MethodDelete, "http://localhost:8000/book/2d1a8ebd-9d9c-4122-87f6-d5e305d6e46e", nil) 74 | 75 | client := &http.Client{} 76 | 77 | resp, _ := client.Do(req) 78 | 79 | if resp.StatusCode != 200{ 80 | t.Errorf("invalid request!") 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /models/author.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/aymane-smi/api-test/utils" 8 | ) 9 | 10 | //can be used as DTO 11 | type Author struct{ 12 | Id int 13 | Full_name string 14 | 15 | } 16 | 17 | 18 | func AddAuthor(a Author) (string, error){ 19 | db := utils.GetInstance() 20 | stmt, err := db.Prepare("INSERT INTO authors(full_name) VALUES($1)") 21 | if err != nil{ 22 | utils.Log.Error(err.Error()) 23 | return "", err 24 | } 25 | if _, err := stmt.Exec(a.Full_name); err != nil{ 26 | utils.Log.Error(err.Error()) 27 | return "", err 28 | } 29 | return "new row inserted in authors", nil 30 | 31 | } 32 | 33 | func AddAuthorTesting(a Author) (string, error){ 34 | db := utils.GetInstance() 35 | stmt, err := db.Prepare("INSERT INTO authors(id, full_name) VALUES(1, $1)") 36 | if err != nil{ 37 | utils.Log.Error(err.Error()) 38 | return "", err 39 | } 40 | if _, err := stmt.Exec(a.Full_name); err != nil{ 41 | utils.Log.Error(err.Error()) 42 | return "", err 43 | } 44 | return "new row inserted in authors", nil 45 | 46 | } 47 | 48 | func UpdateAuthor(a Author) (*Author, error){ 49 | db := utils.GetInstance() 50 | stmt, err := db.Prepare("UPDATE authors SET full_name= $1 WHERE id = $2") 51 | if err != nil { 52 | utils.Log.Error(err.Error()) 53 | return nil, err 54 | } 55 | 56 | if _, err := stmt.Exec(a.Full_name, a.Id); err != nil{ 57 | return nil, err 58 | } 59 | 60 | return &a, err 61 | } 62 | 63 | func GetAuthorById(id int) *Author{ 64 | db := utils.GetInstance() 65 | stmt, err := db.Prepare("SELECT * FROM authors WHERE id = $1") 66 | if err != nil{ 67 | utils.Log.Error(err.Error()) 68 | return nil 69 | } 70 | 71 | row := stmt.QueryRow(id) 72 | var tmp_author Author 73 | if err := row.Scan(&tmp_author.Id, &tmp_author.Full_name); err != nil{ 74 | utils.Log.Error(err.Error()) 75 | return nil 76 | } 77 | return &tmp_author 78 | 79 | } 80 | 81 | func DeleteAuthorById(id int) (string, error){ 82 | db := utils.GetInstance() 83 | stmt, err := db.Prepare("DELETE FROM authors WHERE id = $1") 84 | if err != nil{ 85 | fmt.Println("prepare error") 86 | utils.Log.Error(err.Error()) 87 | return "", err 88 | } 89 | x, _ := stmt.Exec(id); 90 | rowsAffected, err := x.RowsAffected() 91 | if rowsAffected == 0 || err != nil{ 92 | utils.Log.Error("invalid author id to delete") 93 | return "", errors.New("invalid id") 94 | } 95 | 96 | return fmt.Sprintf("book with id '%d' was deleted", id), nil 97 | } -------------------------------------------------------------------------------- /models/book.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aymane-smi/api-test/utils" 7 | "github.com/google/uuid" 8 | ) 9 | 10 | //can be used as DTO 11 | type Book struct{ 12 | Id string 13 | ISBF string 14 | Title string 15 | Page int 16 | Author int 17 | } 18 | 19 | //get book using his ID 20 | 21 | func GetBookById(id string) *Book{ 22 | db := utils.GetInstance() 23 | stmt, err := db.Prepare("SELECT * FROM books WHERE id = $1") 24 | if err != nil{ 25 | utils.Log.Error(err.Error()) 26 | return nil 27 | } 28 | 29 | row := stmt.QueryRow(id) 30 | var tmp_book Book 31 | if err := row.Scan(&tmp_book.Id, &tmp_book.ISBF, &tmp_book.Title, &tmp_book.Page, &tmp_book.Author); err != nil{ 32 | utils.Log.Error(err.Error()) 33 | return nil 34 | } 35 | return &tmp_book 36 | 37 | } 38 | 39 | //add a book by passing a book to the function 40 | 41 | func AddBook(b Book) (string, error){ 42 | newUUID := uuid.New() 43 | db := utils.GetInstance() 44 | stmt, err := db.Prepare("INSERT INTO books(id, isbf, title, page, author_id) VALUES($1, $2, $3, $4, $5)") 45 | if err != nil{ 46 | utils.Log.Error(err.Error()) 47 | return "", err 48 | } 49 | if _, err := stmt.Exec(newUUID.String(), b.ISBF, b.Title, b.Page, b.Author); err != nil{ 50 | utils.Log.Error(err.Error()) 51 | return "", err 52 | } 53 | return "new row inserted in books", nil 54 | 55 | } 56 | 57 | //add a book by passing a book to the function for testing purpose 58 | 59 | func AddBookTest(b Book) (string, error){ 60 | newUUID := "test_id" 61 | db := utils.GetInstance() 62 | stmt, err := db.Prepare("INSERT INTO books(id, isbf, title, page, author_id) VALUES($1, $2, $3, $4, $5)") 63 | if err != nil{ 64 | utils.Log.Error(err.Error()) 65 | return "", err 66 | } 67 | if _, err := stmt.Exec(newUUID, b.ISBF, b.Title, b.Page, b.Author); err != nil{ 68 | utils.Log.Error(err.Error()) 69 | return "", err 70 | } 71 | return "new row inserted in books", nil 72 | 73 | } 74 | 75 | //update a book by passing new tems to change in the records 76 | 77 | func UpdateBook(b Book) (*Book, error){ 78 | db := utils.GetInstance() 79 | stmt, err := db.Prepare("UPDATE books SET title = $1, isbf = $2, page = $3 WHERE id = $4") 80 | if err != nil { 81 | utils.Log.Error(err.Error()) 82 | return nil, err 83 | } 84 | 85 | if _, err := stmt.Exec(b.Title, b.ISBF, b.Page, b.Id); err != nil{ 86 | utils.Log.Error(err.Error()) 87 | return nil, err 88 | } 89 | 90 | return &b, err 91 | } 92 | 93 | func DeleteById(id string) (string, error){ 94 | db := utils.GetInstance() 95 | stmt, err := db.Prepare("DELETE FROM books WHERE id = $1") 96 | if err != nil{ 97 | utils.Log.Error(err.Error()) 98 | return "", err 99 | } 100 | if _, err := stmt.Exec(id); err != nil{ 101 | utils.Log.Error(err.Error()) 102 | return "", err 103 | } 104 | 105 | return fmt.Sprintf("book with id '%s' was deleted", id), nil 106 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # booky cloud native application 2 | 3 | ***application created for learning purpose in order to learn about cloud native*** 4 | 5 | ![workflow for master](https://github.com/aymane-smi/booky/actions/workflows/app-ci.yaml/badge.svg?branch=master) ![tags](https://img.shields.io/github/v/tag/aymane-smi/booky) ![GitHub release (with filter)](https://img.shields.io/github/v/release/aymane-smi/booky) 6 | 7 | 8 | ## application architecture overview 9 | 10 | ![diagram](goapi-v1.png) 11 | ## api documentation 12 | 13 | ### Book api 14 | 15 | | Endpoint | Method | Description | body | response | 16 | |-------------------------|--------|---------------------------------------------------| ---- | -------- | 17 | | `/book/{id}` | GET | Retrieve information about a book by its ID. | `-` | `Book{id, isbf, title, author_id}` 18 | | `/book` | POST | Add a new book to the system. | `Book{isbf, title, author_id}` | `string message` or `error` 19 | | `/book` | PUT | Update an existing book's information. | `Book{isbf, title, author_id}` | `Book{id, isbf, title, author_id}` | 20 | | `/book/{id}` | DELETE | Delete a book by its ID. | `-` | `string message` or `error` 21 | 22 | ### Author api 23 | 24 | | Endpoint | Method | Description | body | response | 25 | |-------------------------|--------|---------------------------------------------------| ---- | -------- | 26 | | `/author/{id}` | GET | Retrieve information about a author by its ID. | `-` | `Author{id, isbf, title, author_id}` 27 | | `/author` | POST | Add a new author to the system. | `Author{isbf, title, author_id}` | `string message` or `error` 28 | | `/author` | PUT | Update an existing author's information. | `Author{isbf, title, author_id}` | `Author{id, isbf, title, author_id}` | 29 | | `/author/{id}` | DELETE | Delete a author by its ID. | `-` | `string message` or `error` 30 | 31 | ### controllers & models test 32 | 33 | you can check both `book_test.go` or `author_test.go` for testing or adding more test to the packages 34 | 35 | ### logging 36 | 37 | for logging system *Booky* uses **Zap** which a fast logger built for go.every logs are stored in ```/logs/api.log``` if you want to change the logging configurations please check ```./utils/log.go``` 38 | 39 | ### monitoring 40 | 41 | the monitoring system use **Prometheus** with 3 types of metrics: 42 | - Counter: which used for determine number of request and errors in the whole application 43 | - Histogram: used for display request response time 44 | 45 | ***checkout `/prometheus/prometheus.go` to see the configuration*** 46 | 47 | #### visualiation: 48 | you can use prometheus server(downlaod the binary file or use docker image) or use grafana. 49 | for grafana please run docker compose file `docker-compose up -d` and add prometheus as datasource.[you can see the docs](https://prometheus.io/docs/tutorials/visualizing_metrics_using_grafana/) 50 | 51 | ### Testing 52 | 53 | in order to make the logger work for both testing and other environment we can change ``WORK_MODE`` in `.env` file to be ```DEV``` or ```TEST``` in the following destination : 54 | - `/models/.env` 55 | - `/.env` 56 | 57 | ### Load Balancing 58 | 59 | for load balancing Booky uses **Nginx** which is a famous proxy server in the market. 60 | 61 | the workflowof the load balancer is explained in the illustration above 62 | 63 | ### Commands 64 | 65 | you can use `Makfile` to run, build, or stop project containers 66 | 67 | ### Build & CICD 68 | 69 | in this project Booky use github action FOR CICD pipeline to test, build release and tags and finally push the project image to docker hub [you can get the official image here](https://hub.docker.com/r/aymanebel/booky). -------------------------------------------------------------------------------- /.github/workflows/app-ci.yaml: -------------------------------------------------------------------------------- 1 | name: Application Testing 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.*' 7 | branches: 8 | - master 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | test: 15 | strategy: 16 | matrix: 17 | go-version: [ 1.17.x, 1.18.x, 1.19.x ] 18 | runs-on: ubuntu-latest 19 | services: 20 | book_db: 21 | image: postgres 22 | env: 23 | POSTGRES_USER: ${{ secrets.PG_USER }} 24 | POSTGRES_DB: booky_go 25 | POSTGRES_PASSWORD: ${{ secrets.PG_PASS }} 26 | ports: 27 | - 5432:5432 28 | options: >- 29 | --health-cmd pg_isready 30 | --health-interval 10s 31 | --health-timeout 5s 32 | --health-retries 5 33 | steps: 34 | - name: Install Go 35 | uses: actions/setup-go@v2 36 | with: 37 | go-version: ${{ matrix.go-version }} 38 | - name: Checkout Repository 39 | uses: actions/checkout@v2 40 | - name: Lint with golangci-lint 41 | uses: golangci/golangci-lint-action@v2 42 | - name: Install Dependency 43 | run: go mod download 44 | - name: Run migrations 45 | run: | 46 | export PGPASSWORD=${{ secrets.PG_PASS }} 47 | psql -h localhost -U ${{ secrets.PG_USER }} -a -f book_go.sql booky_go 48 | - name: Run Go Test with excluding controllers package 49 | env: 50 | DB_HOST: "localhost" 51 | DB_PASS: ${{ secrets.PG_PASS }} 52 | DB_USER: ${{ secrets.PG_USER }} 53 | DB_NAME: "booky_go" 54 | run: go test $(go list ./... | grep -v /controllers) 55 | release: 56 | needs: test 57 | permissions: 58 | contents: write 59 | if: startsWith(github.ref, 'refs/tags/v') 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@v3 63 | - name: Set RELEASE_VERSION ENV var 64 | run: echo "RELEASE_VERSION=${GITHUB_REF:10}" >> $GITHUB_ENV 65 | - name: Install go with version 1.19 66 | uses: actions/setup-go@v2 67 | with: 68 | go-version: 1.19.x 69 | - name: Build binary file 70 | run: go build -o booky 71 | - name: generate release notes 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | run: | 75 | gh api \ 76 | --method POST \ 77 | -H "Accept: application/vnd.github+json" \ 78 | -H "X-GitHub-Api-Version: 2022-11-28" \ 79 | /repos/aymane-smi/booky/releases/generate-notes \ 80 | -f configuration_file_path='.github/release.yaml' \ 81 | -f commitish=${{ env.RELEASE_VERSION }} \ 82 | -f tag_name=${{ env.RELEASE_VERSION }} \ 83 | > tmp-release-notes.json 84 | - name: gzip the bins 85 | run: | 86 | tar -czvf "booky_linux_amd64.tar.gz" -C "./" booky 87 | tar -czvf "booky_linux_arm64.tar.gz" -C "./" booky 88 | - name: create release 89 | env: 90 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 91 | run: | 92 | jq -r .body tmp-release-notes.json > tmp-release-notes.md 93 | gh release create ${{ env.RELEASE_VERSION }} \ 94 | -t "$(jq -r .name tmp-release-notes.json)" \ 95 | -F tmp-release-notes.md \ 96 | "booky_linux_amd64.tar.gz#booky_linux_amd64" \ 97 | "booky_linux_arm64.tar.gz#booky_linux_arm64" 98 | image: 99 | needs: release 100 | runs-on: ubuntu-latest 101 | permissions: 102 | contents: read 103 | packages: write 104 | steps: 105 | - uses: actions/checkout@v3 106 | - name: set env 107 | run: echo "RELEASE_VERSION=${GITHUB_REF:11}" >> $GITHUB_ENV # refs/tags/v1.0.0 substring starting at 1.0.0 108 | - name: setup buildx 109 | uses: docker/setup-buildx-action@v1 110 | - name: login to GitHub container registry 111 | uses: docker/login-action@v2 112 | with: 113 | username: ${{ secrets.DOCKER_USER }} 114 | password: ${{ secrets.DOCKER_PASS }} 115 | - name: build and push 116 | uses: docker/build-push-action@v2 117 | with: 118 | push: true 119 | tags: | 120 | aymanebel/booky:latest 121 | aymanebel/booky:${{env.RELEASE_VERSION}} -------------------------------------------------------------------------------- /controllers/book.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/aymane-smi/api-test/models" 9 | prometheus_book "github.com/aymane-smi/api-test/prometheus" 10 | "github.com/aymane-smi/api-test/utils" 11 | "github.com/gorilla/mux" 12 | ) 13 | 14 | func GetBookById(w http.ResponseWriter, r *http.Request){ 15 | 16 | //timestamp of the handler being called 17 | start := time.Now() 18 | 19 | vars := mux.Vars(r) 20 | 21 | //increment the request counter each time a user make a request call 22 | prometheus_book.TotalRequest.Inc() 23 | 24 | 25 | book := models.GetBookById(vars["id"]) 26 | 27 | if book == nil { 28 | response := map[string]interface{}{ 29 | "message": "invalid book id", 30 | } 31 | //increment the error counter each time a user make a request call and raise an error 32 | prometheus_book.TotalErros.WithLabelValues("book", "GetBookById", "controllers").Inc() 33 | jsonResponse, _ := json.Marshal(response) 34 | utils.JsonWriter(w, http.StatusNotFound, jsonResponse) 35 | return 36 | } 37 | 38 | response := map[string]interface{}{ 39 | "book": book, 40 | } 41 | jsonResponse, _ := json.Marshal(response) 42 | //get the remaining timefrom the start and give it to the histogram 43 | duration := time.Since(start).Seconds() 44 | prometheus_book.RequestDuration.Observe(duration) 45 | utils.JsonWriter(w, http.StatusOK, jsonResponse) 46 | } 47 | 48 | func AddBook(w http.ResponseWriter, r *http.Request){ 49 | //timestamp of the handler being called 50 | start := time.Now() 51 | 52 | var book models.Book 53 | 54 | //increment the request counter each time a user make a request call 55 | prometheus_book.TotalRequest.Inc() 56 | 57 | _ = json.NewDecoder(r.Body).Decode(&book) 58 | 59 | msg, err := models.AddBook(book) 60 | 61 | if err != nil{ 62 | response := map[string]interface{}{ 63 | "message": "something went wrong", 64 | } 65 | //increment the error counter each time a user make a request call and raise an error 66 | prometheus_book.TotalErros.WithLabelValues("book", "AddBook", "controllers").Inc() 67 | jsonResponse, _ := json.Marshal(response) 68 | utils.JsonWriter(w, http.StatusInternalServerError, jsonResponse) 69 | return 70 | } 71 | response := map[string]interface{}{ 72 | "message": msg, 73 | } 74 | jsonResponse, _ := json.Marshal(response) 75 | //get the remaining timefrom the start and give it to the histogram 76 | duration := time.Since(start).Seconds() 77 | prometheus_book.RequestDuration.Observe(duration) 78 | utils.JsonWriter(w, http.StatusOK, jsonResponse) 79 | 80 | } 81 | 82 | func UpdateBook(w http.ResponseWriter, r *http.Request){ 83 | //timestamp of the handler being called 84 | start := time.Now() 85 | 86 | var book models.Book 87 | 88 | //increment the request counter each time a user make a request call 89 | prometheus_book.TotalRequest.Inc() 90 | 91 | _ = json.NewDecoder(r.Body).Decode(&book) 92 | 93 | b, err := models.UpdateBook(book) 94 | 95 | if err != nil{ 96 | response := map[string]interface{}{ 97 | "message": "something went wrong", 98 | } 99 | //increment the error counter each time a user make a request call and raise an error 100 | prometheus_book.TotalErros.WithLabelValues("book", "UpdateBook", "controllers").Inc() 101 | jsonResponse, _ := json.Marshal(response) 102 | utils.JsonWriter(w, http.StatusInternalServerError, jsonResponse) 103 | return 104 | } 105 | response := map[string]interface{}{ 106 | "book": b, 107 | } 108 | jsonResponse, _ := json.Marshal(response) 109 | //get the remaining timefrom the start and give it to the histogram 110 | duration := time.Since(start).Seconds() 111 | prometheus_book.RequestDuration.Observe(duration) 112 | utils.JsonWriter(w, http.StatusOK, jsonResponse) 113 | } 114 | 115 | func DeleteBook(w http.ResponseWriter, r *http.Request){ 116 | //timestamp of the handler being called 117 | start := time.Now() 118 | vars := mux.Vars(r) 119 | 120 | //increment the request counter each time a user make a request call 121 | prometheus_book.TotalRequest.Inc() 122 | 123 | msg, err := models.DeleteById(vars["id"]) 124 | 125 | if err != nil{ 126 | response := map[string]interface{}{ 127 | "message": "invalid book id", 128 | } 129 | //increment the error counter each time a user make a request call and raise an error 130 | prometheus_book.TotalErros.WithLabelValues("book", "DeleteBook", "controllers").Inc() 131 | jsonResponse, _ := json.Marshal(response) 132 | utils.JsonWriter(w, http.StatusInternalServerError, jsonResponse) 133 | return 134 | } 135 | response := map[string]interface{}{ 136 | "message": msg, 137 | } 138 | jsonResponse, _ := json.Marshal(response) 139 | //get the remaining timefrom the start and give it to the histogram 140 | duration := time.Since(start).Seconds() 141 | prometheus_book.RequestDuration.Observe(duration) 142 | utils.JsonWriter(w, http.StatusOK, jsonResponse) 143 | } -------------------------------------------------------------------------------- /controllers/author.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/aymane-smi/api-test/models" 10 | prometheus_book "github.com/aymane-smi/api-test/prometheus" 11 | "github.com/aymane-smi/api-test/utils" 12 | "github.com/gorilla/mux" 13 | ) 14 | 15 | func AddAuthor(w http.ResponseWriter, r *http.Request){ 16 | //timestamp of the handler being called 17 | start := time.Now() 18 | var author models.Author 19 | 20 | //increment the request counter each time a user make a request call 21 | prometheus_book.TotalRequest.Inc() 22 | 23 | _ = json.NewDecoder(r.Body).Decode(&author) 24 | 25 | msg, err := models.AddAuthor(author) 26 | 27 | if err != nil{ 28 | response := map[string]interface{}{ 29 | "message": "something went wrong", 30 | } 31 | //increment the error counter each time a user make a request call and raise an error 32 | prometheus_book.TotalErros.WithLabelValues("author", "AddAuthor", "controllers").Inc() 33 | jsonResponse, _ := json.Marshal(response) 34 | utils.JsonWriter(w, http.StatusInternalServerError, jsonResponse) 35 | return 36 | } 37 | response := map[string]interface{}{ 38 | "message": msg, 39 | } 40 | jsonResponse, _ := json.Marshal(response) 41 | //get the remaining timefrom the start and give it to the histogram 42 | duration := time.Since(start).Seconds() 43 | prometheus_book.RequestDuration.Observe(duration) 44 | utils.JsonWriter(w, http.StatusOK, jsonResponse) 45 | } 46 | 47 | func GetAuthorById(w http.ResponseWriter, r *http.Request){ 48 | //timestamp of the handler being called 49 | start := time.Now() 50 | vars := mux.Vars(r) 51 | 52 | //increment the request counter each time a user make a request call 53 | prometheus_book.TotalRequest.Inc() 54 | 55 | id,_ := strconv.Atoi(vars["id"]) 56 | 57 | author := models.GetAuthorById(id) 58 | 59 | if author == nil { 60 | response := map[string]interface{}{ 61 | "message": "invalid author id", 62 | } 63 | //increment the error counter each time a user make a request call and raise an error 64 | prometheus_book.TotalErros.WithLabelValues("author", "GetAuthorById", "controllers").Inc() 65 | jsonResponse, _ := json.Marshal(response) 66 | utils.JsonWriter(w, http.StatusNotFound, jsonResponse) 67 | return 68 | } 69 | 70 | response := map[string]interface{}{ 71 | "author": author, 72 | } 73 | jsonResponse, _ := json.Marshal(response) 74 | //get the remaining timefrom the start and give it to the histogram 75 | duration := time.Since(start).Seconds() 76 | prometheus_book.RequestDuration.Observe(duration) 77 | utils.JsonWriter(w, http.StatusOK, jsonResponse) 78 | } 79 | 80 | func UpdateAuthor(w http.ResponseWriter, r *http.Request){ 81 | //timestamp of the handler being called 82 | start := time.Now() 83 | var author models.Author 84 | 85 | //increment the request counter each time a user make a request call 86 | prometheus_book.TotalRequest.Inc() 87 | 88 | _ = json.NewDecoder(r.Body).Decode(&author) 89 | 90 | a, err := models.UpdateAuthor(author) 91 | 92 | if err != nil{ 93 | response := map[string]interface{}{ 94 | "message": "something went wrong", 95 | } 96 | //increment the error counter each time a user make a request call and raise an error 97 | prometheus_book.TotalErros.WithLabelValues("author", "UpdateAuthor", "controllers").Inc() 98 | jsonResponse, _ := json.Marshal(response) 99 | utils.JsonWriter(w, http.StatusInternalServerError, jsonResponse) 100 | return 101 | } 102 | response := map[string]interface{}{ 103 | "author": a, 104 | } 105 | jsonResponse, _ := json.Marshal(response) 106 | //get the remaining timefrom the start and give it to the histogram 107 | duration := time.Since(start).Seconds() 108 | prometheus_book.RequestDuration.Observe(duration) 109 | utils.JsonWriter(w, http.StatusOK, jsonResponse) 110 | } 111 | 112 | func DeleteAuthor(w http.ResponseWriter, r *http.Request){ 113 | //timestamp of the handler being called 114 | start := time.Now() 115 | 116 | vars := mux.Vars(r) 117 | 118 | //increment the request counter each time a user make a request call 119 | prometheus_book.TotalRequest.Inc() 120 | 121 | id,_ := strconv.Atoi(vars["id"]) 122 | 123 | msg, err := models.DeleteAuthorById(id) 124 | 125 | if err != nil{ 126 | response := map[string]interface{}{ 127 | "message": "invalid author id", 128 | } 129 | //increment the error counter each time a user make a request call and raise an error 130 | prometheus_book.TotalErros.WithLabelValues("author", "DeleteAuthor", "controllers").Inc() 131 | jsonResponse, _ := json.Marshal(response) 132 | utils.JsonWriter(w, http.StatusInternalServerError, jsonResponse) 133 | return 134 | } 135 | response := map[string]interface{}{ 136 | "message": msg, 137 | } 138 | jsonResponse, _ := json.Marshal(response) 139 | //get the remaining timefrom the start and give it to the histogram 140 | duration := time.Since(start).Seconds() 141 | prometheus_book.RequestDuration.Observe(duration) 142 | utils.JsonWriter(w, http.StatusOK, jsonResponse) 143 | } -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= 2 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 3 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 4 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 5 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 9 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 10 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 11 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 12 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 13 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 14 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 15 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 16 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 17 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 18 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 19 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 20 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 21 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 22 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 23 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 24 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 25 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 26 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 27 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 28 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 29 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 30 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 31 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 32 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 | github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= 34 | github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= 35 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 36 | github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 37 | github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= 38 | github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= 39 | github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= 40 | github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= 41 | github.com/steinfletcher/apitest v1.5.15 h1:AAdTN0yMbf0VMH/PMt9uB2I7jljepO6i+5uhm1PjH3c= 42 | github.com/steinfletcher/apitest v1.5.15/go.mod h1:mF+KnYaIkuHM0C4JgGzkIIOJAEjo+EA5tTjJ+bHXnQc= 43 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 44 | go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= 45 | go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= 46 | go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 47 | go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= 48 | go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= 49 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 50 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 51 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 52 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 53 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 54 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 55 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 56 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 57 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 58 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 59 | -------------------------------------------------------------------------------- /logs/api.log: -------------------------------------------------------------------------------- 1 | {"level":"info","logged at":"2023-08-12T12:32:07.537+0100","message":"logger construction succeeded","foo":"bar"} 2 | {"level":"info","logged at":"2023-08-12T12:32:19.509+0100","message":"logger construction succeeded","foo":"bar"} 3 | {"level":"info","logged at":"2023-08-12T12:32:27.365+0100","message":"logger construction succeeded","foo":"bar"} 4 | {"level":"info","logged at":"2023-08-12T12:32:51.723+0100","message":"logger construction succeeded","foo":"bar"} 5 | {"level":"info","logged at":"2023-08-12T12:33:03.897+0100","message":"logger construction succeeded","foo":"bar"} 6 | {"level":"info","logged at":"2023-08-12T12:34:17.341+0100","message":"logger construction succeeded","foo":"bar"} 7 | {"level":"info","logged at":"2023-08-12T12:44:13.748+0100","message":"logger construction succeeded","foo":"bar"} 8 | {"level":"info","logged at":"2023-08-12T12:52:18.019+0100","message":"logger construction succeeded","foo":"bar"} 9 | {"level":"info","logged at":"2023-08-12T13:00:01.308+0100","message":"logger construction succeeded","foo":"bar"} 10 | {"level":"info","logged at":"2023-08-12T20:22:24.408+0100","message":"logger construction succeeded","foo":"bar"} 11 | {"level":"info","logged at":"2023-08-12T20:38:55.609+0100","message":"logger construction succeeded","foo":"bar"} 12 | {"level":"error","logged at":"2023-08-13T01:20:49.056+0100","message":"sql: no rows in result set","foo":"bar"} 13 | {"level":"info","logged at":"2023-08-13T01:21:03.948+0100","message":"logger construction succeeded","foo":"bar"} 14 | {"level":"error","logged at":"2023-08-13T01:21:09.212+0100","message":"sql: no rows in result set","foo":"bar"} 15 | {"level":"error","logged at":"2023-08-13T01:21:47.011+0100","message":"sql: no rows in result set","foo":"bar"} 16 | {"level":"info","logged at":"2023-08-13T01:30:23.619+0100","message":"logger construction succeeded","foo":"bar"} 17 | {"level":"error","logged at":"2023-08-13T01:31:00.556+0100","message":"sql: no rows in result set","foo":"bar"} 18 | {"level":"info","logged at":"2023-08-21T11:44:11.865+0100","message":"logger construction succeeded","foo":"bar"} 19 | {"level":"info","logged at":"2023-08-21T16:54:36.520+0100","message":"logger construction succeeded","foo":"bar"} 20 | {"level":"error","logged at":"2023-08-21T16:54:36.551+0100","message":"pq: insert or update on table \"books\" violates foreign key constraint \"books_author_id_fkey\"","foo":"bar"} 21 | {"level":"info","logged at":"2023-08-21T16:59:45.522+0100","message":"logger construction succeeded","foo":"bar"} 22 | {"level":"info","logged at":"2023-08-21T17:00:17.387+0100","message":"logger construction succeeded","foo":"bar"} 23 | {"level":"info","logged at":"2023-08-21T18:35:13.448+0100","message":"logger construction succeeded","foo":"bar"} 24 | {"level":"error","logged at":"2023-08-21T18:35:13.501+0100","message":"sql: no rows in result set","foo":"bar"} 25 | {"level":"error","logged at":"2023-08-21T18:35:13.511+0100","message":"invalid author id to delete","foo":"bar"} 26 | {"level":"info","logged at":"2023-08-21T18:35:13.511+0100","message":"logger construction succeeded","foo":"bar"} 27 | {"level":"error","logged at":"2023-08-21T18:35:13.536+0100","message":"sql: no rows in result set","foo":"bar"} 28 | {"level":"info","logged at":"2023-08-21T18:35:13.536+0100","message":"logger construction succeeded","foo":"bar"} 29 | {"level":"info","logged at":"2023-08-21T18:35:13.572+0100","message":"logger construction succeeded","foo":"bar"} 30 | {"level":"error","logged at":"2023-08-21T18:35:13.600+0100","message":"sql: no rows in result set","foo":"bar"} 31 | {"level":"info","logged at":"2023-08-21T18:36:49.961+0100","message":"logger construction succeeded","foo":"bar"} 32 | {"level":"error","logged at":"2023-08-21T18:36:50.186+0100","message":"sql: no rows in result set","foo":"bar"} 33 | {"level":"error","logged at":"2023-08-21T18:36:50.193+0100","message":"invalid author id to delete","foo":"bar"} 34 | {"level":"info","logged at":"2023-08-21T18:36:50.193+0100","message":"logger construction succeeded","foo":"bar"} 35 | {"level":"error","logged at":"2023-08-21T18:36:50.221+0100","message":"sql: no rows in result set","foo":"bar"} 36 | {"level":"info","logged at":"2023-08-21T18:36:50.221+0100","message":"logger construction succeeded","foo":"bar"} 37 | {"level":"error","logged at":"2023-08-21T18:36:50.249+0100","message":"pq: insert or update on table \"books\" violates foreign key constraint \"books_author_id_fkey\"","foo":"bar"} 38 | {"level":"info","logged at":"2023-08-21T18:36:50.250+0100","message":"logger construction succeeded","foo":"bar"} 39 | {"level":"error","logged at":"2023-08-21T18:36:50.287+0100","message":"sql: no rows in result set","foo":"bar"} 40 | {"level":"info","logged at":"2023-08-21T18:39:28.055+0100","message":"logger construction succeeded","foo":"bar"} 41 | {"level":"info","logged at":"2023-08-21T18:39:28.312+0100","message":"logger construction succeeded","foo":"bar"} 42 | {"level":"error","logged at":"2023-08-21T18:39:28.340+0100","message":"sql: no rows in result set","foo":"bar"} 43 | {"level":"info","logged at":"2023-08-21T18:39:28.341+0100","message":"logger construction succeeded","foo":"bar"} 44 | {"level":"error","logged at":"2023-08-21T18:39:28.368+0100","message":"pq: insert or update on table \"books\" violates foreign key constraint \"books_author_id_fkey\"","foo":"bar"} 45 | {"level":"info","logged at":"2023-08-21T18:39:28.369+0100","message":"logger construction succeeded","foo":"bar"} 46 | {"level":"error","logged at":"2023-08-21T18:39:28.404+0100","message":"sql: no rows in result set","foo":"bar"} 47 | {"level":"info","logged at":"2023-08-21T18:44:29.469+0100","message":"logger construction succeeded","foo":"bar"} 48 | {"level":"info","logged at":"2023-08-21T18:45:16.635+0100","message":"logger construction succeeded","foo":"bar"} 49 | {"level":"error","logged at":"2023-08-21T18:45:16.663+0100","message":"pq: insert or update on table \"books\" violates foreign key constraint \"books_author_id_fkey\"","foo":"bar"} 50 | {"level":"info","logged at":"2023-08-21T19:00:07.203+0100","message":"logger construction succeeded","foo":"bar"} 51 | {"level":"info","logged at":"2023-08-21T19:00:48.222+0100","message":"logger construction succeeded","foo":"bar"} 52 | {"level":"error","logged at":"2023-08-21T19:00:48.250+0100","message":"pq: insert or update on table \"books\" violates foreign key constraint \"books_author_id_fkey\"","foo":"bar"} 53 | {"level":"info","logged at":"2023-08-21T19:01:04.998+0100","message":"logger construction succeeded","foo":"bar"} 54 | {"level":"info","logged at":"2023-08-21T19:01:39.122+0100","message":"logger construction succeeded","foo":"bar"} 55 | {"level":"info","logged at":"2023-08-21T19:01:39.336+0100","message":"logger construction succeeded","foo":"bar"} 56 | {"level":"info","logged at":"2023-08-21T19:02:26.263+0100","message":"logger construction succeeded","foo":"bar"} 57 | {"level":"info","logged at":"2023-08-21T19:02:26.477+0100","message":"logger construction succeeded","foo":"bar"} 58 | {"level":"info","logged at":"2023-08-21T19:02:26.556+0100","message":"logger construction succeeded","foo":"bar"} 59 | {"level":"info","logged at":"2023-08-21T19:02:26.581+0100","message":"logger construction succeeded","foo":"bar"} 60 | {"level":"info","logged at":"2023-08-21T19:02:26.616+0100","message":"logger construction succeeded","foo":"bar"} 61 | {"level":"info","logged at":"2023-08-22T15:51:58.510+0100","message":"logger construction succeeded","foo":"bar"} 62 | {"level":"info","logged at":"2023-08-22T16:56:57.679+0100","message":"logger construction succeeded","foo":"bar"} 63 | {"level":"error","logged at":"2023-08-22T16:56:57.714+0100","message":"pq: password authentication failed for user \"aymane\"","foo":"bar"} 64 | {"level":"info","logged at":"2023-08-22T17:02:51.956+0100","message":"logger construction succeeded","foo":"bar"} 65 | {"level":"info","logged at":"2023-08-22T17:03:45.970+0100","message":"logger construction succeeded","foo":"bar"} 66 | {"level":"error","logged at":"2023-08-22T17:03:45.998+0100","message":"pq: password authentication failed for user \"aymane\"","foo":"bar"} 67 | {"level":"info","logged at":"2023-08-22T17:03:45.998+0100","message":"logger construction succeeded","foo":"bar"} 68 | {"level":"error","logged at":"2023-08-22T17:03:46.021+0100","message":"pq: password authentication failed for user \"aymane\"","foo":"bar"} 69 | {"level":"error","logged at":"2023-08-22T17:03:46.026+0100","message":"pq: password authentication failed for user \"aymane\"","foo":"bar"} 70 | {"level":"error","logged at":"2023-08-22T17:03:46.030+0100","message":"pq: password authentication failed for user \"aymane\"","foo":"bar"} 71 | {"level":"error","logged at":"2023-08-22T17:03:46.035+0100","message":"pq: password authentication failed for user \"aymane\"","foo":"bar"} 72 | {"level":"info","logged at":"2023-08-22T17:03:46.035+0100","message":"logger construction succeeded","foo":"bar"} 73 | {"level":"error","logged at":"2023-08-22T17:03:46.058+0100","message":"pq: password authentication failed for user \"aymane\"","foo":"bar"} 74 | {"level":"info","logged at":"2023-08-22T17:03:46.058+0100","message":"logger construction succeeded","foo":"bar"} 75 | {"level":"error","logged at":"2023-08-22T17:03:46.080+0100","message":"pq: password authentication failed for user \"aymane\"","foo":"bar"} 76 | {"level":"info","logged at":"2023-08-22T17:03:46.081+0100","message":"logger construction succeeded","foo":"bar"} 77 | {"level":"error","logged at":"2023-08-22T17:03:46.104+0100","message":"pq: password authentication failed for user \"aymane\"","foo":"bar"} 78 | {"level":"info","logged at":"2023-08-22T17:03:46.104+0100","message":"logger construction succeeded","foo":"bar"} 79 | {"level":"error","logged at":"2023-08-22T17:03:46.130+0100","message":"pq: password authentication failed for user \"aymane\"","foo":"bar"} 80 | {"level":"error","logged at":"2023-08-22T17:03:46.134+0100","message":"pq: password authentication failed for user \"aymane\"","foo":"bar"} 81 | {"level":"info","logged at":"2023-08-25T00:54:08.520+0100","message":"logger construction succeeded","foo":"bar"} 82 | {"level":"info","logged at":"2023-08-25T00:56:01.826+0100","message":"logger construction succeeded","foo":"bar"} 83 | {"level":"info","logged at":"2023-08-25T01:29:31.297+0100","message":"logger construction succeeded","foo":"bar"} 84 | --------------------------------------------------------------------------------