diff --git a/.github/workflows/devopstest.yml b/.github/workflows/devopstest.yml index b8f9581..d802777 100644 --- a/.github/workflows/devopstest.yml +++ b/.github/workflows/devopstest.yml @@ -4,7 +4,7 @@ on: pull_request: push: branches: - - increment3 + - main jobs: diff --git a/.github/workflows/statictest.yml b/.github/workflows/statictest.yml index 682aa46..9c7df43 100644 --- a/.github/workflows/statictest.yml +++ b/.github/workflows/statictest.yml @@ -4,7 +4,7 @@ on: pull_request: push: branches: - - increment3 + - main jobs: diff --git a/cmd/agent/main.go b/cmd/agent/main.go index 1f07540..62c0b00 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -1,15 +1,31 @@ package main import ( + "flag" "log" + "time" + "github.com/caarlos0/env/v6" "github.com/ncyellow/devops/internal/agent" + "github.com/ncyellow/devops/internal/agent/config" ) func main() { - conf := agent.Config{ - Host: "localhost:8080", + + var cfg config.Config + + flag.StringVar(&cfg.Address, "a", "127.0.0.1:8080", "address in the format host:port") + flag.DurationVar(&cfg.ReportInterval, "r", time.Second*10, "send to server interval in the format 10s") + flag.DurationVar(&cfg.PollInterval, "p", time.Second*2, "polling metrics interval in the format 2s") + // Сначала аргументы командной строки + flag.Parse() + + // Далее более приоритетные от ENV + err := env.Parse(&cfg) + if err != nil { + log.Fatal(err) } - collector := agent.Agent{Conf: conf} + + collector := agent.Agent{Conf: cfg} log.Fatal(collector.Run()) } diff --git a/cmd/server/main.go b/cmd/server/main.go index 0e7300c..316e503 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,27 +1,32 @@ package main import ( + "flag" "log" - "net/http" - "os" - "os/signal" - "syscall" + "time" - "github.com/ncyellow/devops/internal/server/handlers" - "github.com/ncyellow/devops/internal/server/storage" + "github.com/caarlos0/env/v6" + "github.com/ncyellow/devops/internal/server" + "github.com/ncyellow/devops/internal/server/config" ) func main() { - repo := storage.NewRepository() - r := handlers.NewRouter(repo) - done := make(chan os.Signal, 1) - signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + var cfg config.Config - go func() { - if err := http.ListenAndServe(":8080", r); err != nil && err != http.ErrServerClosed { - log.Fatalf("listen: %s\n", err) - } - }() - <-done + flag.StringVar(&cfg.Address, "a", "localhost:8080", "address in the format host:port") + flag.DurationVar(&cfg.StoreInterval, "i", time.Second*300, "store interval in the format 300s") + flag.BoolVar(&cfg.Restore, "r", true, "restore from file. true if needed") + flag.StringVar(&cfg.StoreFile, "f", "/tmp/devops-metrics-db.json", "filename that used for save metrics state") + // Сначала парсим командную строку + flag.Parse() + + // Далее приоритетно аргументы из ENV + err := env.Parse(&cfg) + if err != nil { + log.Fatal(err) + } + + server := server.Server{Conf: cfg} + server.RunServer() } diff --git a/go.mod b/go.mod index 0d99724..aec82f9 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,21 @@ module github.com/ncyellow/devops go 1.18 require ( - github.com/davecgh/go-spew v1.1.0 // indirect + github.com/caarlos0/env/v6 v6.9.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/go-chi/chi/v5 v5.0.7 // indirect + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect + github.com/nxadm/tail v1.4.8 // indirect + github.com/onsi/ginkgo v1.16.5 // indirect + github.com/onsi/gomega v1.19.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.7.1 // indirect + golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect + golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum index d1a1206..5ffd4c8 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,97 @@ +github.com/caarlos0/env/v6 v6.9.3 h1:Tyg69hoVXDnpO5Qvpsu8EoquarbPyQb+YwExWHP8wWU= +github.com/caarlos0/env/v6 v6.9.3/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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/agent/agent.go b/internal/agent/agent.go index 4d3a067..f375dd9 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -1,6 +1,8 @@ package agent import ( + "bytes" + "encoding/json" "fmt" "log" "math/rand" @@ -10,21 +12,13 @@ import ( "runtime" "syscall" "time" -) -const ( - pollInterval = time.Second * 2 - reportInterval = time.Second * 10 + "github.com/ncyellow/devops/internal/agent/config" + "github.com/ncyellow/devops/internal/server/storage" ) -// Config содержит параметры по настройке агента -type Config struct { - // Host строка в формате localhost:8080 - Host string -} - -// Metrics текущее состояние всех метрик обновляются с интервалом pollInterval -type Metrics struct { +// RuntimeMetrics текущее состояние всех метрик обновляются с интервалом pollInterval +type RuntimeMetrics struct { PollCount int64 RandomValue float64 runtime.MemStats @@ -33,42 +27,27 @@ type Metrics struct { // Agent опрашивает метрики и отправляет их на сервер с интервалом reportInterval. // Пример запуска: // conf := agent.Config{ -// Host: "localhost:8080", +// Address: "localhost:8080", // } // collector := agent.Agent{Conf: conf} type Agent struct { - Conf Config - metrics Metrics + Conf config.Config + metrics RuntimeMetrics } // sendToServer отправка метрик на сервер func (collector *Agent) sendToServer() { //! приводим все метрики к нужным типам. - gauges := prepareGauges(&collector.metrics) - for name, value := range gauges { - url := fmt.Sprintf("http://%s/update/gauge/%s/%f", collector.Conf.Host, name, value) - resp, err := http.Post(url, "text/plain", nil) - if err != nil { - log.Fatal(err) - } - resp.Body.Close() - } - - counters := prepareCounters(&collector.metrics) - for name, value := range counters { - url := fmt.Sprintf("http://%s/update/counter/%s/%d", collector.Conf.Host, name, value) - resp, err := http.Post(url, "text/plain", nil) - if err != nil { - log.Fatal(err) - } - resp.Body.Close() - } + url := fmt.Sprintf("http://%s/update/", collector.Conf.Address) + SendMetrics(collector.metrics.prepareGauges(), url) + SendMetrics(collector.metrics.prepareCounters(), url) } // Run запускает цикл по обработке таймеров и ожидания сигналов от ОС func (collector *Agent) Run() error { - tickerPoll := time.NewTicker(pollInterval) - tickerReport := time.NewTicker(reportInterval) + + tickerPoll := time.NewTicker(collector.Conf.PollInterval) + tickerReport := time.NewTicker(collector.Conf.ReportInterval) defer tickerPoll.Stop() defer tickerReport.Stop() @@ -102,8 +81,8 @@ func (collector *Agent) Run() error { // prepareGauges - готовит map[string]float64 с метриками gauges для отправки на сервер, // так как класс метрики довольно жирный передает через указатель -func prepareGauges(metrics *Metrics) map[string]float64 { - return map[string]float64{ +func (metrics *RuntimeMetrics) prepareGauges() []storage.Metrics { + gauges := map[string]float64{ "Alloc": float64(metrics.Alloc), "BuckHashSys": float64(metrics.BuckHashSys), "Frees": float64(metrics.Frees), @@ -133,12 +112,54 @@ func prepareGauges(metrics *Metrics) map[string]float64 { "TotalAlloc": float64(metrics.TotalAlloc), "RandomValue": metrics.RandomValue, } + + result := make([]storage.Metrics, 0, len(gauges)) + for name, value := range gauges { + // Если пользоваться value, то все значения будут ссылаться на одну и ту же переменную - последнюю + gaugeValue := value + metric := storage.Metrics{ + ID: name, + MType: storage.Gauge, + Value: &gaugeValue, + } + result = append(result, metric) + } + return result } // prepareCounters - готовит map[string]int64 с метриками counter для отправки на сервер, // пока такая метрика одна, но для обобщения сделан сразу метод -func prepareCounters(metrics *Metrics) map[string]int64 { - return map[string]int64{ +func (metrics *RuntimeMetrics) prepareCounters() []storage.Metrics { + counters := map[string]int64{ "PollCount": metrics.PollCount, } + + result := make([]storage.Metrics, 0, len(counters)) + for name, value := range counters { + // Если пользоваться value, то все значения будут ссылаться на одну и ту же переменную - последнюю + counterValue := value + metric := storage.Metrics{ + ID: name, + MType: storage.Counter, + Delta: &counterValue, + } + result = append(result, metric) + } + return result +} + +// SendMetrics отправляет метрики на указанный url +func SendMetrics(dataSource []storage.Metrics, url string) { + for _, metric := range dataSource { + buf, err := json.Marshal(metric) + if err != nil { + log.Fatal(err) + } + resp, err := http.Post(url, "application/json", bytes.NewBuffer(buf)) + if err != nil { + log.Println(err) + continue + } + resp.Body.Close() + } } diff --git a/internal/agent/config/config.go b/internal/agent/config/config.go new file mode 100644 index 0000000..e8dbc66 --- /dev/null +++ b/internal/agent/config/config.go @@ -0,0 +1,10 @@ +package config + +import "time" + +// Config содержит параметры по настройке агента +type Config struct { + Address string `env:"ADDRESS"` + ReportInterval time.Duration `env:"REPORT_INTERVAL"` + PollInterval time.Duration `env:"POLL_INTERVAL"` +} diff --git a/internal/server/config/config.go b/internal/server/config/config.go new file mode 100644 index 0000000..f24a69e --- /dev/null +++ b/internal/server/config/config.go @@ -0,0 +1,11 @@ +package config + +import "time" + +// Config конфигурационные параметры сервера. +type Config struct { + Address string `env:"ADDRESS"` + StoreInterval time.Duration `env:"STORE_INTERVAL"` + StoreFile string `env:"STORE_FILE"` + Restore bool `env:"RESTORE"` +} diff --git a/internal/server/handlers/handlers.go b/internal/server/handlers/handlers.go index 430d984..b0cbe36 100644 --- a/internal/server/handlers/handlers.go +++ b/internal/server/handlers/handlers.go @@ -1,12 +1,15 @@ package handlers import ( + "encoding/json" "fmt" + "io/ioutil" "net/http" "strconv" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "github.com/ncyellow/devops/internal/server/middlewares" "github.com/ncyellow/devops/internal/server/storage" ) @@ -14,10 +17,13 @@ import ( func NewRouter(repo storage.Repository) chi.Router { r := chi.NewRouter() r.Use(middleware.Recoverer) + r.Use(middlewares.EncoderGZIP) r.Get("/", ListHandler(repo)) r.Get("/value/{metricType}/{metricName}", ValueHandler(repo)) r.Post("/update/{metricType}/{metricName}/{metricValue}", UpdateHandler(repo)) + r.Post("/update/", UpdateJSONHandler(repo)) + r.Post("/value/", ValueJSONHandler(repo)) return r } @@ -93,7 +99,7 @@ func UpdateHandler(repo storage.Repository) http.HandlerFunc { //! Сейчас проблема только одна - ошибка при кривом имени метрики if err != nil { rw.WriteHeader(http.StatusInternalServerError) - rw.Write([]byte("incorrect metric name ")) + rw.Write([]byte("incorrect metric name")) return } default: @@ -110,7 +116,90 @@ func UpdateHandler(repo storage.Repository) http.HandlerFunc { // ListHandler обрабатывает GET запросы на корень url. Возвращает список всех метрик + значение func ListHandler(repo storage.Repository) http.HandlerFunc { return func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "text/html") rw.WriteHeader(http.StatusOK) rw.Write([]byte(repo.String())) } } + +// UpdateJSONHandler обрабатывает POST запросы на обновление метрик в виде json +func UpdateJSONHandler(repo storage.Repository) http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + + if r.Header.Get("Content-Type") != "application/json" { + rw.WriteHeader(http.StatusInternalServerError) + rw.Write([]byte("content type not support")) + return + } + reqBody, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + rw.Write([]byte("Read data problem")) + return + } + metric := storage.Metrics{} + err = json.Unmarshal(reqBody, &metric) + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + rw.Write([]byte("invalid deserialization")) + return + } + + err = repo.UpdateMetric(metric) + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + rw.Write([]byte("incorrect metric type")) + return + } + + rw.WriteHeader(http.StatusOK) + rw.Write([]byte("ok")) + } +} + +// ValueJSONHandler обрабатывает POST запрос, который возвращает список всех метрик в виде json +func ValueJSONHandler(repo storage.Repository) http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + + if r.Header.Get("Content-Type") != "application/json" { + rw.WriteHeader(http.StatusInternalServerError) + rw.Write([]byte("content type not support")) + return + } + + reqBody, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + rw.Write([]byte("Read data problem")) + return + } + + metric := storage.Metrics{} + err = json.Unmarshal(reqBody, &metric) + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + rw.Write([]byte("invalid deserialization")) + return + } + + metricType := metric.MType + metricName := metric.ID + + val, ok := repo.Metric(metricName, metricType) + if ok { + result, ok := json.Marshal(val) + if ok != nil { + rw.WriteHeader(http.StatusInternalServerError) + rw.Write([]byte("invalid serialization")) + return + } + rw.Header().Set("Content-Type", "application/json") + rw.Write(result) + return + } + + rw.WriteHeader(http.StatusNotFound) + } +} diff --git a/internal/server/handlers/handlers_test.go b/internal/server/handlers/handlers_test.go index a85432a..9291700 100644 --- a/internal/server/handlers/handlers_test.go +++ b/internal/server/handlers/handlers_test.go @@ -1,6 +1,7 @@ package handlers import ( + "bytes" "fmt" "io/ioutil" "net/http" @@ -10,12 +11,60 @@ import ( "github.com/ncyellow/devops/internal/server/storage" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" ) -func testRequest(t *testing.T, ts *httptest.Server, method, path string) (*http.Response, string) { - req, err := http.NewRequest(method, ts.URL+path, nil) +type want struct { + statusCode int + body string +} +type tests struct { + name string + request string + requestType string + contentType string + body []byte + want want +} + +type HandlersSuite struct { + suite.Suite + ts *httptest.Server +} + +// SetupSuite перед началом теста стартуем новый сервер httptest.Server делаем так, чтобы тестировать каждый +// handler отдельно и не сливать все тесты в один +func (suite *HandlersSuite) SetupTest() { + repo := storage.NewRepository() + r := NewRouter(repo) + + suite.ts = httptest.NewServer(r) +} + +// TearDownSuite после теста отключаем сервер +func (suite *HandlersSuite) TearDownTest() { + suite.ts.Close() +} + +func TestHandlersSuite(t *testing.T) { + suite.Run(t, new(HandlersSuite)) +} + +func (suite *HandlersSuite) runTableTests(testList []tests) { + for _, tt := range testList { + resp, body := runTestRequest(suite.T(), suite.ts, tt.requestType, tt.request, tt.contentType, tt.body) + assert.Equal(suite.T(), tt.want.statusCode, resp.StatusCode, tt.name) + assert.Equal(suite.T(), tt.want.body, body, tt.name) + resp.Body.Close() + } +} + +func runTestRequest(t *testing.T, ts *httptest.Server, method, path string, contentType string, reqBody []byte) (*http.Response, string) { + req, err := http.NewRequest(method, ts.URL+path, bytes.NewBuffer(reqBody)) require.NoError(t, err) + req.Header.Set("Content-Type", contentType) + resp, err := http.DefaultClient.Do(req) require.NoError(t, err) @@ -25,91 +74,92 @@ func testRequest(t *testing.T, ts *httptest.Server, method, path string) (*http. return resp, string(respBody) } -// TestRouter тесты по запросам к различным url -func TestRouter(t *testing.T) { - repo := storage.NewRepository() - r := NewRouter(repo) - - ts := httptest.NewServer(r) - defer ts.Close() - - type want struct { - statusCode int - body string - } - tests := []struct { - name string - request string - requestType string - want want - }{ +//TestListHandler тестируем ListHandler +func (suite *HandlersSuite) TestListHandler() { + testData := []tests{ { - name: "add counter metric with correct data", - request: "/update/counter/testCounter/100", + name: "add gauge metric with correct data", + request: "/update/gauge/testGauge/100", requestType: "POST", + contentType: "text/plain", + body: nil, want: want{ statusCode: http.StatusOK, body: "ok", }, }, { - name: "add counter metric without id", - request: "/update/counter/", + name: "add counter metric with correct data", + request: "/update/counter/testCounter/100", requestType: "POST", + contentType: "text/plain", + body: nil, want: want{ - statusCode: http.StatusNotFound, - body: "404 page not found\n", + statusCode: http.StatusOK, + body: "ok", }, }, { - name: "counter invalid value", - request: "/update/counter/testCounter/none", - requestType: "POST", + name: "list all metrics", + request: "/", + requestType: "GET", + contentType: "text/plain", + body: nil, want: want{ - statusCode: http.StatusBadRequest, - body: "incorrect metric value", + statusCode: http.StatusOK, + body: ` + + +

All metrics

+

gauges

+ +

counters

+ + + `, }, }, + } + suite.runTableTests(testData) +} + +//TestListHandler тестируем ValueHandler +func (suite *HandlersSuite) TestValueHandler() { + testData := []tests{ { name: "add gauge metric with correct data", request: "/update/gauge/testGauge/100", requestType: "POST", + contentType: "text/plain", + body: nil, want: want{ statusCode: http.StatusOK, body: "ok", }, }, { - name: "add gauge metric without id", - request: "/update/gauge/", - requestType: "POST", - want: want{ - statusCode: http.StatusNotFound, - body: "404 page not found\n", - }, - }, - { - name: "gauge invalid value", - request: "/update/gauge/testGauge/none", - requestType: "POST", - want: want{ - statusCode: http.StatusBadRequest, - body: "incorrect metric value", - }, - }, - { - name: "invalid update type", - request: "/update/unknown/testCounter/100", + name: "add counter metric with correct data", + request: "/update/counter/testCounter/100", requestType: "POST", + contentType: "text/plain", + body: nil, want: want{ - statusCode: http.StatusNotImplemented, - body: "incorrect metric type", + statusCode: http.StatusOK, + body: "ok", }, }, { name: "get correct counter value", request: "/value/counter/testCounter", requestType: "GET", + contentType: "text/plain", + body: nil, want: want{ statusCode: http.StatusOK, body: "100", @@ -119,6 +169,8 @@ func TestRouter(t *testing.T) { name: "get unknown counter value", request: "/value/counter/unknownCounter", requestType: "GET", + contentType: "text/plain", + body: nil, want: want{ statusCode: http.StatusNotFound, body: "", @@ -128,6 +180,8 @@ func TestRouter(t *testing.T) { name: "get correct gauge value", request: "/value/gauge/testGauge", requestType: "GET", + contentType: "text/plain", + body: nil, want: want{ statusCode: http.StatusOK, body: fmt.Sprintf("%.3f", 100.0), @@ -137,42 +191,115 @@ func TestRouter(t *testing.T) { name: "get unknown gauge value", request: "/value/gauge/unknownGauge", requestType: "GET", + contentType: "text/plain", + body: nil, want: want{ statusCode: http.StatusNotFound, body: "", }, }, + } + suite.runTableTests(testData) +} + +//TestListHandler тестируем UpdateHandler +func (suite *HandlersSuite) TestUpdateHandler() { + testData := []tests{ { - name: "list all metrics", - request: "/", + name: "add gauge metric with correct data", + request: "/update/gauge/testGauge/100", + requestType: "POST", + contentType: "text/plain", + body: nil, + want: want{ + statusCode: http.StatusOK, + body: "ok", + }, + }, + { + name: "add counter metric with correct data", + request: "/update/counter/testCounter/100", + requestType: "POST", + contentType: "text/plain", + body: nil, + want: want{ + statusCode: http.StatusOK, + body: "ok", + }, + }, + { + name: "get correct counter value", + request: "/value/counter/testCounter", requestType: "GET", + contentType: "text/plain", + body: nil, want: want{ statusCode: http.StatusOK, - body: ` - - -

All metrics

-

gauges

- -

counters

- - - `, + body: "100", + }, + }, + { + name: "get correct gauge value", + request: "/value/gauge/testGauge", + requestType: "GET", + contentType: "text/plain", + body: nil, + want: want{ + statusCode: http.StatusOK, + body: fmt.Sprintf("%.3f", 100.0), }, }, } + suite.runTableTests(testData) +} - for _, tt := range tests { - resp, body := testRequest(t, ts, tt.requestType, tt.request) - assert.Equal(t, tt.want.statusCode, resp.StatusCode, tt.name) - assert.Equal(t, tt.want.body, body, tt.name) - resp.Body.Close() +// TestUpdateValueJSONHandler тестируем UpdateJSONHandler ValueJSONHandler +func (suite *HandlersSuite) TestUpdateValueJSONHandler() { + testData := []tests{ + { + name: "set gauge with json", + request: "/update/", + requestType: "POST", + contentType: "application/json", + body: []byte(`{"id":"jsonGauge","type":"gauge","value": 111}`), + want: want{ + statusCode: http.StatusOK, + body: "ok", + }, + }, + { + name: "set counter with json", + request: "/update/", + requestType: "POST", + contentType: "application/json", + body: []byte(`{"id":"jsonCounter","type":"counter","delta": 123}`), + want: want{ + statusCode: http.StatusOK, + body: "ok", + }, + }, + { + name: "test get gauge with json", + request: "/value/", + requestType: "POST", + contentType: "application/json", + body: []byte(`{"id":"jsonGauge","type":"gauge"}`), + want: want{ + statusCode: http.StatusOK, + body: `{"id":"jsonGauge","type":"gauge","value":111}`, + }, + }, + { + name: "test get counter with json", + request: "/value/", + requestType: "POST", + contentType: "application/json", + body: []byte(`{"id":"jsonCounter","type":"counter"}`), + want: want{ + statusCode: http.StatusOK, + body: `{"id":"jsonCounter","type":"counter","delta":123}`, + }, + }, } - + suite.runTableTests(testData) } diff --git a/internal/server/middlewares/gzipencoder.go b/internal/server/middlewares/gzipencoder.go new file mode 100644 index 0000000..5f27214 --- /dev/null +++ b/internal/server/middlewares/gzipencoder.go @@ -0,0 +1,40 @@ +package middlewares + +import ( + "compress/gzip" + "io" + "net/http" + "strings" +) + +type gzipWriter struct { + http.ResponseWriter + Writer io.Writer +} + +func (w gzipWriter) Write(b []byte) (int, error) { + return w.Writer.Write(b) +} + +// EncoderGZIP middleware для поддержки gzip +func EncoderGZIP(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // проверяем, что клиент поддерживает gzip-сжатие + if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { + next.ServeHTTP(w, r) + return + } + + // создаём gzip.Writer поверх текущего w + gz, err := gzip.NewWriterLevel(w, gzip.BestSpeed) + if err != nil { + io.WriteString(w, err.Error()) + return + } + defer gz.Close() + + w.Header().Set("Content-Encoding", "gzip") + // передаём обработчику страницы переменную типа gzipWriter для вывода данных + next.ServeHTTP(gzipWriter{ResponseWriter: w, Writer: gz}, r) + }) +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..eeffbee --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,43 @@ +package server + +import ( + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + + "github.com/ncyellow/devops/internal/server/config" + "github.com/ncyellow/devops/internal/server/handlers" + "github.com/ncyellow/devops/internal/server/storage" +) + +type Server struct { + Conf config.Config +} + +func (s Server) RunServer() { + repo := storage.NewRepository() + + if s.Conf.Restore { + err := storage.RestoreFromFile(s.Conf.StoreFile, repo) + if err != nil { + fmt.Println(err) + } + } + + r := handlers.NewRouter(repo) + + done := make(chan os.Signal, 1) + signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + go func() { + if err := http.ListenAndServe(s.Conf.Address, r); err != nil && err != http.ErrServerClosed { + log.Fatalf("listen: %s\n", err) + } + }() + go storage.RunStorageSaver(s.Conf, repo) + + <-done +} diff --git a/internal/server/storage/memory.go b/internal/server/storage/memory.go index a27244c..e12ba6a 100644 --- a/internal/server/storage/memory.go +++ b/internal/server/storage/memory.go @@ -1,6 +1,7 @@ package storage import ( + "encoding/json" "fmt" "sync" ) @@ -40,6 +41,15 @@ func (s *MapRepository) UpdateCounter(name string, value int64) error { return nil } +func (s *MapRepository) UpdateMetric(metric Metrics) error { + if metric.MType == Gauge { + return s.UpdateGauge(metric.ID, *metric.Value) + } else if metric.MType == Counter { + return s.UpdateCounter(metric.ID, *metric.Delta) + } + return fmt.Errorf("metric with type %s doesn't exsist", metric.MType) +} + func (s *MapRepository) Gauge(name string) (val float64, ok bool) { s.gaugesLock.RLock() val, ok = s.gauges[name] @@ -55,6 +65,34 @@ func (s *MapRepository) Counter(name string) (val int64, ok bool) { return } +func (s *MapRepository) Metric(name string, mType string) (val Metrics, ok bool) { + if mType == Gauge { + val, ok := s.Gauge(name) + if !ok { + return Metrics{}, ok + } + return Metrics{ + ID: name, + MType: mType, + Value: &val, + Delta: nil, + }, ok + } + if mType == Counter { + val, ok := s.Counter(name) + if !ok { + return Metrics{}, ok + } + return Metrics{ + ID: name, + MType: mType, + Value: nil, + Delta: &val, + }, ok + } + return Metrics{}, false +} + func (s *MapRepository) String() string { htmlTmpl := ` @@ -87,3 +125,69 @@ func (s *MapRepository) String() string { return fmt.Sprintf(htmlTmpl, gaugesText, countersText) } + +// toMetrics Конвертация данных MapRepository в []Metrics +func (s *MapRepository) toMetrics() []Metrics { + totalCount := len(s.gauges) + len(s.counters) + metrics := make([]Metrics, 0, totalCount) + + s.gaugesLock.RLock() + for name, value := range s.gauges { + gaugeValue := value + metrics = append(metrics, Metrics{ + ID: name, + MType: Gauge, + Value: &gaugeValue, + }) + } + s.gaugesLock.RUnlock() + + s.countersLock.RLock() + for name, value := range s.counters { + counterValue := value + metrics = append(metrics, Metrics{ + ID: name, + MType: Counter, + Delta: &counterValue, + }) + } + s.countersLock.RUnlock() + return metrics +} + +// fromMetrics - обновляет метрики в MapRepository по []Metrics +func (s *MapRepository) fromMetrics(metrics []Metrics) { + for _, metric := range metrics { + switch metric.MType { + case Gauge: + if metric.Value != nil { + s.UpdateGauge(metric.ID, *metric.Value) + } + case Counter: + if metric.Delta != nil { + s.UpdateCounter(metric.ID, *metric.Delta) + } + } + } +} + +// MarshalJSON - реализация интерфейса Marshaler +func (s *MapRepository) MarshalJSON() ([]byte, error) { + metrics := s.toMetrics() + jsMetrics, err := json.Marshal(metrics) + if err != nil { + return []byte{}, nil + } + return jsMetrics, nil +} + +// UnmarshalJSON - реализация интерфейса Unmarshaler +func (s *MapRepository) UnmarshalJSON(data []byte) error { + var metrics []Metrics + err := json.Unmarshal(data, &metrics) + if err != nil { + return err + } + s.fromMetrics(metrics) + return nil +} diff --git a/internal/server/storage/memory_test.go b/internal/server/storage/memory_test.go index 6831304..2896046 100644 --- a/internal/server/storage/memory_test.go +++ b/internal/server/storage/memory_test.go @@ -1,63 +1,221 @@ +// Тесты для MapRepository package storage import ( - "testing" - - "github.com/stretchr/testify/assert" + "encoding/json" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" ) -// TestMapRepository Тестируем вставку и чтение в MapRepository для gauge -func TestMapRepositoryGauge(t *testing.T) { - repo := NewRepository() - - // обновление - err := repo.UpdateGauge("testGauge", 100.0) - assert.NoError(t, err) - - // чтение - val, ok := repo.Gauge("testGauge") - assert.Equal(t, val, 100.0) - assert.Equal(t, ok, true) - - // обновляем повторно - err = repo.UpdateGauge("testGauge", 300.0) - assert.NoError(t, err) - - // проверяем что старое значение перезаписалось - val, ok = repo.Gauge("testGauge") - assert.Equal(t, val, 300.0) - assert.Equal(t, ok, true) - - // Проверка чтения неизвестного значения - _, ok = repo.Gauge("unknownGauge") - assert.Equal(t, ok, false) - -} - -// TestMapRepository Тестируем вставку и чтение в MapRepository для counter -func TestMapRepositoryCounter(t *testing.T) { - repo := NewRepository() - - // обновление - err := repo.UpdateCounter("testCounter", 100) - assert.NoError(t, err) - - // чтение - val, ok := repo.Counter("testCounter") - assert.Equal(t, val, int64(100)) - assert.Equal(t, ok, true) - - // обновляем еще раз - err = repo.UpdateCounter("testCounter", 100) - assert.NoError(t, err) - - // проверяем что счетчик приплюсовал значение - val, ok = repo.Counter("testCounter") - assert.Equal(t, val, int64(200)) - assert.Equal(t, ok, true) - - // Проверка чтения неизвестного значения - _, ok = repo.Counter("unknownCounter") - assert.Equal(t, ok, false) - -} +var _ = Describe("MapRepository", func() { + Context("Если выполняется сериализация в json MapRepository", func() { + It("json должен быть корректным и содержать все значения из MapRepository", func() { + repo := NewRepository() + + err := repo.UpdateGauge("testGaugeMetric", 100) + Expect(err).Should(BeNil()) + + err = repo.UpdateCounter("testCounterMetric", 120) + Expect(err).Should(BeNil()) + + jsRepo, err := json.Marshal(repo) + Expect(err).Should(BeNil()) + + Expect(jsRepo).Should(MatchJSON(`[{"id":"testGaugeMetric","type":"gauge","value":100},{"id":"testCounterMetric","type":"counter","delta":120}]`)) + }) + }) + + Context("Если выполняется десериализация json в MapRepository", func() { + It("Разобраные метрики должны соответствовать исходным значениям", func() { + repo := NewRepository() + data := []byte(`[{"id":"testGaugeMetric","type":"gauge","value":100},{"id":"testCounterMetric","type":"counter","delta":120}]`) + + err := json.Unmarshal(data, &repo) + Expect(err).Should(BeNil()) + + val, ok := repo.Gauge("testGaugeMetric") + Expect(ok).Should(Equal(true)) + Expect(val).Should(Equal(100.0)) + + delta, ok := repo.Counter("testCounterMetric") + Expect(ok).Should(Equal(true)) + Expect(delta).Should(Equal(int64(120))) + }) + }) + + Context("Если передан некорректный json", func() { + It("Unmarshal завершаться с ошибкой", func() { + brokenRepo := NewRepository() + brokenData := []byte(`{"name": "Joe", "age": null, }`) + + err := json.Unmarshal(brokenData, &brokenRepo) + Expect(err).ShouldNot(BeNil()) + }) + }) + + Context("Выполняем проверку работы gauge метрик", func() { + It("Проверка простого чтения и записи через API Metric", func() { + repo := NewRepository() + + var updateValue float64 = 100 + + // обновление + err := repo.UpdateMetric(Metrics{ + ID: "testGaugeMetric", + MType: Gauge, + Value: &updateValue, + }) + Expect(err).Should(BeNil()) + + // чтение + val, ok := repo.Metric("testGaugeMetric", Gauge) + Expect(ok).Should(Equal(true)) + Expect(*val.Value).Should(Equal(updateValue)) + }) + + It("После повторного обновления gauge должно быть выставлено последнее значение", func() { + repo := NewRepository() + // обновление + var updateValue float64 = 100 + + // обновление + err := repo.UpdateMetric(Metrics{ + ID: "testGaugeMetric", + MType: Gauge, + Value: &updateValue, + }) + Expect(err).Should(BeNil()) + + // обновляем повторно + updateValue = 300 + err = repo.UpdateMetric(Metrics{ + ID: "testGaugeMetric", + MType: Gauge, + Value: &updateValue, + }) + Expect(err).Should(BeNil()) + + // проверяем что старое значение перезаписалось + val, ok := repo.Metric("testGaugeMetric", Gauge) + Expect(*val.Value).Should(Equal(updateValue)) + Expect(ok).Should(Equal(true)) + }) + + It("Для неизвестного типа возвращается ошибка метрик", func() { + repo := NewRepository() + updateValue := 100.0 + err := repo.UpdateMetric(Metrics{ + ID: "testMetric", + MType: "unknownType", + Value: &updateValue, + }) + Expect(err).ShouldNot(BeNil()) + }) + + It("Чтение неизвестной метрики типа gauge возвращает Metrics{}, false", func() { + repo := NewRepository() + // Проверка чтения неизвестной метрики тика Gauge + val, ok := repo.Metric("unknownMetricGauge", Gauge) + Expect(ok).Should(BeFalse()) + Expect(val).Should(Equal(Metrics{})) + }) + }) + + Context("Выполняем проверку работы counter метрик", func() { + + It("Чтение неизвестной метрики типа counter возвращает Metrics{}, false", func() { + repo := NewRepository() + // Проверка чтения неизвестной метрики тика Gauge + val, ok := repo.Metric("unknownMetricCounter", Counter) + Expect(ok).Should(BeFalse()) + Expect(val).Should(Equal(Metrics{})) + }) + + It("Проверка стандартного чтения записи counter метрик", func() { + repo := NewRepository() + + // обновление + err := repo.UpdateCounter("testCounter", 100) + Expect(err).Should(BeNil()) + + // чтение + val, ok := repo.Counter("testCounter") + Expect(ok).Should(Equal(true)) + Expect(val).Should(Equal(int64(100))) + }) + It("Проверка что два обновления метрики увеличивает счетчик counter", func() { + repo := NewRepository() + + // обновление + err := repo.UpdateCounter("testCounter", 100) + Expect(err).Should(BeNil()) + + // обновление + err = repo.UpdateCounter("testCounter", 100) + Expect(err).Should(BeNil()) + + // чтение + val, ok := repo.Counter("testCounter") + Expect(ok).Should(Equal(true)) + Expect(val).Should(Equal(int64(200))) + }) + + It("Проверка чтения неизвестной метрики counter", func() { + repo := NewRepository() + // Проверка чтения неизвестного значения + _, ok := repo.Counter("unknownCounter") + Expect(ok).Should(Equal(false)) + }) + + It("Проверка стандартного чтения записи counter метрик через API Metrics", func() { + repo := NewRepository() + + var updateValue int64 = 100 + + // обновление + err := repo.UpdateMetric(Metrics{ + ID: "testCounterMetric", + MType: Counter, + Delta: &updateValue, + }) + Expect(err).Should(BeNil()) + + // чтение + val, ok := repo.Metric("testCounterMetric", Counter) + Expect(ok).Should(Equal(true)) + Expect(*val.Delta).Should(Equal(updateValue)) + }) + }) + + Context("Проверяем интерфейс Stringer", func() { + It("Должен быть корректный html наличии метрик gauge и counter", func() { + repo := NewRepository() + + // обновление + err := repo.UpdateGauge("testGauge", 100.0) + Expect(err).Should(BeNil()) + + err = repo.UpdateCounter("testCounter", 100) + Expect(err).Should(BeNil()) + + correctHTML := ` + + +

All metrics

+

gauges

+ +

counters

+ + + ` + + Expect(repo.String()).Should(Equal(correctHTML)) + }) + }) +}) diff --git a/internal/server/storage/storage.go b/internal/server/storage/storage.go index a191c14..7629ab0 100644 --- a/internal/server/storage/storage.go +++ b/internal/server/storage/storage.go @@ -7,6 +7,17 @@ const ( Counter = "counter" ) +type Metrics struct { + // Имя метрики + ID string `json:"id"` + // Параметр, принимающий значение gauge или counter + MType string `json:"type"` + // Значение метрики в случае передачи counter + Delta *int64 `json:"delta,omitempty"` + // Значение метрики в случае передачи gauge + Value *float64 `json:"value,omitempty"` +} + // Repository содержит API для работы с хранилищем метрик. type Repository interface { // UpdateGauge обновить значение метрики типа gauge @@ -19,6 +30,12 @@ type Repository interface { // Counter возвращает текущее значение метрики типа counter Counter(name string) (val int64, ok bool) + // Metric возвращает значение метрики по названию + Metric(name string, mType string) (val Metrics, ok bool) + + // UpdateMetric обновляет данные в хранилище по значению Metrics + UpdateMetric(metrics Metrics) error + // Stringer Вывод в строку всех метрик хранилища fmt.Stringer } diff --git a/internal/server/storage/storage_saver.go b/internal/server/storage/storage_saver.go new file mode 100644 index 0000000..c6c625d --- /dev/null +++ b/internal/server/storage/storage_saver.go @@ -0,0 +1,53 @@ +package storage + +import ( + "encoding/json" + "os" + "time" + + "github.com/ncyellow/devops/internal/server/config" +) + +// SaveToFile сохраняет данные repo в файл с именем fileName +func SaveToFile(fileName string, repo Repository) error { + //! Если файл не задан, ок ничего не делаем + if fileName == "" { + return nil + } + + file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777) + if err != nil { + return err + } + defer file.Close() + encoder := json.NewEncoder(file) + return encoder.Encode(&repo) +} + +// RestoreFromFile загружает данные в repo из файла с именем fileName +func RestoreFromFile(fileName string, repo Repository) error { + file, err := os.OpenFile(fileName, os.O_RDONLY, 0777) + if err != nil { + return err + } + defer file.Close() + decoder := json.NewDecoder(file) + return decoder.Decode(&repo) +} + +// RunStorageSaver запускает сохранение данных repo по таймеру в файл +func RunStorageSaver(config config.Config, repo Repository) { + if config.StoreInterval == 0 { + //! Не нужно сбрасывать на диск если StoreInterval == 0 + return + } + + tickerStore := time.NewTicker(config.StoreInterval) + defer tickerStore.Stop() + + for { + <-tickerStore.C + //! сбрасываем на диск + _ = SaveToFile(config.StoreFile, repo) + } +} diff --git a/internal/server/storage/storage_saver_test.go b/internal/server/storage/storage_saver_test.go new file mode 100644 index 0000000..40dc8a9 --- /dev/null +++ b/internal/server/storage/storage_saver_test.go @@ -0,0 +1,61 @@ +package storage + +import ( + "encoding/json" + "os" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestStorageSaver(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Storage Saver Suite") +} + +var _ = Describe("Storage Saver", func() { + + repo := NewRepository() + var fileName string + + BeforeSuite(func() { + data := []byte(`[{"id":"testGaugeMetric","type":"gauge","value":100},{"id":"testCounterMetric","type":"counter","delta":120}]`) + err := json.Unmarshal(data, &repo) + Expect(err).Should(BeNil()) + + // Создаем временный файл + file, err := os.CreateTemp(os.TempDir(), "restore*") + Expect(err).Should(BeNil()) + + fileName = file.Name() + // Закрываем так как открывать мы будем его сами + file.Close() + }) + + AfterSuite(func() { + // Удаляем тестовый временный файл + os.Remove(fileName) + }) + + Context("Если при восстановении передан не существующий файл", func() { + It("Метод должен завершаться с ошибкой", func() { + newRepo := NewRepository() + Expect(RestoreFromFile("", newRepo)).ShouldNot(BeNil()) + }) + }) + + Context("Если при сохранении передан левый файл", func() { + It("Метод должен завершаться без ошибки", func() { + Expect(SaveToFile("", repo)).Should(BeNil()) + }) + }) + + It("После сохранения восстановления данные должны быть идентичны", func() { + Expect(SaveToFile(fileName, repo)).Should(BeNil()) + + newRepo := NewRepository() + Expect(RestoreFromFile(fileName, newRepo)).Should(BeNil()) + Expect(repo.String()).Should(Equal(newRepo.String())) + }) +})