commit 8b41b82d24af3f9ca5bba8c380e0e6cead8a0023 Author: Sergey Elpashev Date: Thu Nov 20 13:11:16 2025 +0300 Initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..002aacb --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Code coverage profiles and other test artifacts +*.out +coverage.* +*.coverprofile +profile.cov + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# Editor/IDE +# .idea/ +.vscode/ +!.vscode/settings.json \ No newline at end of file diff --git a/cmd/apiserver/main.go b/cmd/apiserver/main.go new file mode 100644 index 0000000..9f94faa --- /dev/null +++ b/cmd/apiserver/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "flag" + "log" + + "git.nwaifu.su/sergey/MyGoServer/internal/app/apiserver" +) + +var ( + configPath string +) + +func init() { + flag.StringVar(&configPath, "config-path", "/go/bin/configs/server.toml", "path to config") +} + +func main() { + flag.Parse() + if err := apiserver.Start(); err != nil { + log.Fatal(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2e061bd --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module git.nwaifu.su/sergey/MyGoServer + +go 1.25.4 + +require ( + github.com/felixge/httpsnoop v1.0.4 + github.com/gorilla/mux v1.8.1 + github.com/sirupsen/logrus v1.9.3 +) + +require ( + github.com/google/uuid v1.6.0 + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cfed775 --- /dev/null +++ b/go.sum @@ -0,0 +1,21 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/apiserver/apiserver.go b/internal/app/apiserver/apiserver.go new file mode 100644 index 0000000..50fba4f --- /dev/null +++ b/internal/app/apiserver/apiserver.go @@ -0,0 +1,29 @@ +package apiserver + +import ( + "context" + "net" + "net/http" +) + +type contextKey struct { + key string +} + +var connContextKey = &contextKey{"http-conn"} + +func saveConnInContext(ctx context.Context, c net.Conn) context.Context { + return context.WithValue(ctx, connContextKey, c) +} + +// Start the server +func Start() error { + srv := newServer() + server := http.Server{ + Addr: ":8080", + ConnContext: saveConnInContext, + Handler: srv, + } + + return server.ListenAndServe() +} diff --git a/internal/app/apiserver/responsewriter.go b/internal/app/apiserver/responsewriter.go new file mode 100644 index 0000000..1ad3ae5 --- /dev/null +++ b/internal/app/apiserver/responsewriter.go @@ -0,0 +1,39 @@ +package apiserver + +import ( + "bufio" + "errors" + "net" + "net/http" + + "github.com/felixge/httpsnoop" +) + +// Custom RW implementation +type ResponseWriter struct { + http.ResponseWriter + code int + Hijacker http.Hijacker +} + +// Write status code to header +func (w *ResponseWriter) WriteHeader(statusCode int) { + w.code = statusCode + w.ResponseWriter.WriteHeader(statusCode) +} + +// Get new RW +func newResponseWriter(w http.ResponseWriter) *ResponseWriter { + hijacker, _ := w.(http.Hijacker) + return &ResponseWriter{ + ResponseWriter: httpsnoop.Wrap(w, httpsnoop.Hooks{}), + Hijacker: hijacker, + } +} + +func (w *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if w.Hijacker == nil { + return nil, nil, errors.New("http.Hijacker not implemented by underlying http.ResponseWriter") + } + return w.Hijacker.Hijack() +} diff --git a/internal/app/apiserver/server.go b/internal/app/apiserver/server.go new file mode 100644 index 0000000..2125ade --- /dev/null +++ b/internal/app/apiserver/server.go @@ -0,0 +1,81 @@ +package apiserver + +import ( + "context" + "encoding/json" + "net/http" + "time" + + "github.com/google/uuid" + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" +) + +const ( + ctxKeyRequestID = iota +) + +type server struct { + router *mux.Router + logger *logrus.Logger +} + +func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.router.ServeHTTP(w, r) +} + +func newServer() *server { + s := &server{ + router: mux.NewRouter(), + logger: logrus.New(), + } + s.configureRouter() + return s +} + +func (s *server) configureRouter() { + s.router.Use(s.setRequestID) + s.router.Use(s.logRequest) + s.router.HandleFunc("/", s.handleHome()) +} + +func (s *server) handleHome() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("ok")) + } +} + +func (s *server) logRequest(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + logger := logrus.WithFields(logrus.Fields{ + "remote_addr": r.RemoteAddr, + "request_id": r.Context().Value(ctxKeyRequestID), + }) + logger.Infof("started %s %s", r.Method, r.RequestURI) + start := time.Now() + rw := newResponseWriter(w) + rw.code = http.StatusOK + next.ServeHTTP(rw, r) + logger.Infof("completed with %d %s in %v", rw.code, http.StatusText(rw.code), time.Since(start)) + }) +} + +func (s *server) setRequestID(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + id := uuid.New().String() + w.Header().Set("X-Request-ID", id) + next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxKeyRequestID, id))) + }) +} + +// Helpers +func (s *server) error(w http.ResponseWriter, r *http.Request, status int, err error) { + s.respond(w, r, status, map[string]string{"error": err.Error()}) +} + +func (s *server) respond(w http.ResponseWriter, r *http.Request, status int, data interface{}) { + w.WriteHeader(status) + if data != nil { + json.NewEncoder(w).Encode(data) + } +}