diff --git a/db/migrate/000001_create_metrics_table.down.sql b/db/migrate/000001_create_metrics_table.down.sql new file mode 100644 index 0000000..d09a697 --- /dev/null +++ b/db/migrate/000001_create_metrics_table.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS metrics; +DROP TYPE IF EXISTS mkind; \ No newline at end of file diff --git a/db/migrate/000001_create_metrics_table.up.sql b/db/migrate/000001_create_metrics_table.up.sql new file mode 100644 index 0000000..b119569 --- /dev/null +++ b/db/migrate/000001_create_metrics_table.up.sql @@ -0,0 +1,10 @@ +CREATE TYPE metricKind AS ENUM ('counter', 'gauge'); + +CREATE TABLE IF NOT EXISTS metrics( + id varchar(255) primary key, + name varchar(255) not null, + kind metricKind not null, + value double precision not null +); + +CREATE INDEX metrics__idx ON metrics (id); \ No newline at end of file diff --git a/go.mod b/go.mod index a842faa..5ba6052 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ require ( github.com/caarlos0/env/v11 v11.1.0 github.com/go-chi/chi v1.5.5 github.com/go-chi/chi/v5 v5.1.0 + github.com/golang-migrate/migrate/v4 v4.17.1 + github.com/jackc/pgx/v5 v5.6.0 github.com/rs/zerolog v1.33.0 github.com/satori/go.uuid v1.2.0 github.com/spf13/pflag v1.0.5 @@ -14,11 +16,23 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/stretchr/objx v0.5.2 // indirect + go.uber.org/atomic v1.7.0 // indirect + golang.org/x/crypto v0.20.0 // indirect + golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2ee3bb2..23d51e2 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,71 @@ +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/caarlos0/env/v11 v11.1.0 h1:a5qZqieE9ZfzdvbbdhTalRrHT5vu/4V1/ad1Ka6frhI= github.com/caarlos0/env/v11 v11.1.0/go.mod h1:LwgkYk1kDvfGpHthrWWLof3Ny7PezzFwS4QrsJdHTMo= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/dhui/dktest v0.4.1 h1:/w+IWuDXVymg3IrRJCHHOkMK10m9aNVMOyD0X12YVTg= +github.com/dhui/dktest v0.4.1/go.mod h1:DdOqcUpL7vgyP4GlF3X3w7HbSlz8cEQzwewPveYEQbA= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= +github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= +github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= @@ -24,16 +73,35 @@ github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= +golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/entities/errors.go b/internal/entities/errors.go index ffa4393..1beaa3b 100644 --- a/internal/entities/errors.go +++ b/internal/entities/errors.go @@ -14,8 +14,9 @@ var ( ErrMetricMissingValue = errors.New("metric value is missing") ErrMetricInvalidValue = errors.New("metric value is invalid") - ErrStoragePush = errors.New("failed to push record") - ErrStorageFetch = errors.New("failed to get record") + ErrStoragePush = errors.New("failed to push record") + ErrStorageFetch = errors.New("failed to get record") + ErrStorageUnpingable = errors.New("healthcheck is not supported") ErrEncodingInternal = errors.New("internal encoding error") ErrEncodingUnsupported = errors.New("requsted encoding is not supported") diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 1504a19..a97d1a8 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -36,7 +36,7 @@ func (r MetricResource) Homepage(rw http.ResponseWriter, req *http.Request) { body := fmt.Sprintln("mainpage here.") - records, err := r.storageService.List() + records, err := r.storageService.List(ctx) if err != nil { writeErrorResponse(ctx, rw, errToStatus(err), err) return @@ -92,7 +92,7 @@ func (r MetricResource) UpdateMetric(rw http.ResponseWriter, req *http.Request) return } - newRecord, err := r.storageService.Push(record) + newRecord, err := r.storageService.Push(ctx, record) if err != nil { writeErrorResponse(ctx, rw, http.StatusInternalServerError, err) return @@ -125,7 +125,7 @@ func (r MetricResource) UpdateMetricJSON(rw http.ResponseWriter, req *http.Reque return } - newRecord, err := r.storageService.Push(record) + newRecord, err := r.storageService.Push(ctx, record) if err != nil { writeErrorResponse(ctx, rw, http.StatusInternalServerError, err) return @@ -157,7 +157,7 @@ func (r MetricResource) GetMetric(rw http.ResponseWriter, req *http.Request) { } var record storage.Record - record, err := r.storageService.Get(metricName, metricKind) + record, err := r.storageService.Get(ctx, metricName, metricKind) if err != nil { writeErrorResponse(ctx, rw, errToStatus(err), err) return @@ -188,7 +188,7 @@ func (r MetricResource) GetMetricJSON(rw http.ResponseWriter, req *http.Request) return } - record, err := r.storageService.Get(mex.ID, mex.MType) + record, err := r.storageService.Get(ctx, mex.ID, mex.MType) if err != nil { writeErrorResponse(ctx, rw, errToStatus(err), err) return @@ -208,6 +208,32 @@ func (r MetricResource) GetMetricJSON(rw http.ResponseWriter, req *http.Request) } } +type PingerResource struct { + pinger storage.Pinger +} + +func NewPingerResource(pinger storage.Pinger) PingerResource { + return PingerResource{ + pinger: pinger, + } +} + +func (pr PingerResource) Ping(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + err := pr.pinger.Ping(ctx) + if err == nil { + return + } + + if errors.Is(err, entities.ErrStorageUnpingable) { + writeErrorResponse(ctx, w, http.StatusNotImplemented, err) + return + } + + writeErrorResponse(ctx, w, http.StatusInternalServerError, err) +} + func errToStatus(err error) int { switch err { case entities.ErrRecordNotFound, entities.ErrMetricMissingName: diff --git a/internal/server/handlers_test.go b/internal/server/handlers_test.go index 48d9546..b4ecfb0 100644 --- a/internal/server/handlers_test.go +++ b/internal/server/handlers_test.go @@ -38,6 +38,15 @@ func testRequest(t *testing.T, router http.Handler, method, path string, payload return resp.StatusCode, contentType, respBody } +func createTestRouter() (http.Handler, *storage.ServiceMock, *storage.PingerMock) { + sm := &storage.ServiceMock{} + pm := &storage.PingerMock{} + + router := NewRouter(sm, pm) + + return router, sm, pm +} + func TestHomepage(t *testing.T) { type result struct { code int @@ -69,10 +78,8 @@ func TestHomepage(t *testing.T) { } for _, tt := range tests { - m := storage.ServiceMock{} - m.On("List").Return(tt.metrics, nil) - - router := NewRouter(&m) + router, sm, _ := createTestRouter() + sm.On("List").Return(tt.metrics, nil) t.Run(tt.name, func(t *testing.T) { code, _, body := testRequest(t, router, http.MethodGet, tt.path, nil) @@ -164,14 +171,12 @@ func TestUpdateMetric(t *testing.T) { } for _, tt := range tests { - m := new(storage.ServiceMock) + router, sm, _ := createTestRouter() if tt.mock != nil { - tt.mock(m) + tt.mock(sm) } - router := NewRouter(m) - t.Run(tt.name, func(t *testing.T) { code, _, body := testRequest(t, router, http.MethodPost, tt.path, nil) @@ -239,14 +244,12 @@ func TestUpdateJSONMetric(t *testing.T) { assert := assert.New(t) require := require.New(t) - m := storage.ServiceMock{} + router, sm, _ := createTestRouter() if tt.mock != nil { - tt.mock(&m) + tt.mock(sm) } - router := NewRouter(&m) - payload, err := json.Marshal(tt.mex) require.NoError(err) @@ -319,11 +322,10 @@ func TestGetMetric(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - m := storage.ServiceMock{} - router := NewRouter(&m) + router, sm, _ := createTestRouter() if tt.mock != nil { - tt.mock(&m) + tt.mock(sm) } code, _, body := testRequest(t, router, http.MethodGet, tt.path, nil) @@ -426,14 +428,12 @@ func TestGetMetricJSON(t *testing.T) { assert := assert.New(t) require := require.New(t) - m := storage.ServiceMock{} + router, sm, _ := createTestRouter() if tt.mock != nil { - tt.mock(&m) + tt.mock(sm) } - router := NewRouter(&m) - payload, err := json.Marshal(tt.mex) require.NoError(err) @@ -452,3 +452,50 @@ func TestGetMetricJSON(t *testing.T) { }) } } + +func TestPing(t *testing.T) { + type result struct { + code int + } + + tests := []struct { + name string + pingResponse error + expected result + }{ + { + name: "should return ok if storage is ok", + pingResponse: nil, + expected: result{ + code: http.StatusOK, + }, + }, + { + name: "should return not implemented if storage doesn't support ping", + pingResponse: entities.ErrStorageUnpingable, + expected: result{ + code: http.StatusNotImplemented, + }, + }, + { + name: "should return internal server error if storage offline", + pingResponse: entities.ErrUnexpected, + expected: result{ + code: http.StatusInternalServerError, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + router, _, pm := createTestRouter() + pm.On("Ping", mock.Anything).Return(tt.pingResponse) + + code, _, _ := testRequest(t, router, http.MethodGet, "/ping", nil) + + if tt.expected.code != code { + t.Fatalf("expected response to be %d, got: %d", tt.expected.code, code) + } + }) + } +} diff --git a/internal/server/router.go b/internal/server/router.go index e1a86cf..0e0015b 100644 --- a/internal/server/router.go +++ b/internal/server/router.go @@ -10,7 +10,10 @@ import ( "github.com/ex0rcist/metflix/internal/storage" ) -func NewRouter(storageService storage.StorageService) http.Handler { +func NewRouter( + storageService storage.StorageService, + pingerService storage.Pinger, +) http.Handler { router := chi.NewRouter() router.Use(chimdlw.RealIP) @@ -24,6 +27,13 @@ func NewRouter(storageService storage.StorageService) http.Handler { w.WriteHeader(http.StatusNotFound) // no default body })) + registerMetricsEndpoints(storageService, router) + registerPingerEndpoint(pingerService, router) + + return router +} + +func registerMetricsEndpoints(storageService storage.StorageService, router *chi.Mux) { resource := NewMetricResource(storageService) router.Get("/", resource.Homepage) @@ -33,6 +43,10 @@ func NewRouter(storageService storage.StorageService) http.Handler { router.Get("/value/{metricKind}/{metricName}", resource.GetMetric) router.Post("/value", resource.GetMetricJSON) +} - return router +func registerPingerEndpoint(pingerService storage.Pinger, router *chi.Mux) { + resource := NewPingerResource(pingerService) + + router.Get("/ping", resource.Ping) } diff --git a/internal/server/server.go b/internal/server/server.go index 7c1fba1..2d7c5a1 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -25,6 +25,7 @@ type Config struct { StoreInterval int `env:"STORE_INTERVAL"` StorePath string `env:"FILE_STORAGE_PATH"` RestoreOnStart bool `env:"RESTORE"` + DatabaseDSN string `env:"DATABASE_DSN"` } func New() (*Server, error) { @@ -46,7 +47,8 @@ func New() (*Server, error) { } storageService := storage.NewService(dataStorage) - router := NewRouter(storageService) + pingerService := storage.NewPingerService(dataStorage) + router := NewRouter(storageService, pingerService) httpServer := &http.Server{ Addr: config.Address.String(), @@ -82,6 +84,10 @@ func (s *Server) String() string { str = append(str, fmt.Sprintf("restore=%t", s.config.RestoreOnStart)) } + if kind == storage.KindDatabase { + str = append(str, fmt.Sprintf("database=%s", s.config.DatabaseDSN)) + } + return "server config: " + strings.Join(str, "; ") } @@ -109,6 +115,7 @@ func parseFlags(config *Config, progname string, args []string) error { storeInterval := flags.IntP("store-interval", "i", config.StoreInterval, "interval (s) for dumping metrics to the disk, zero value means saving after each request") storePath := flags.StringP("store-file", "f", config.StorePath, "path to file to store metrics") restoreOnStart := flags.BoolP("restore", "r", config.RestoreOnStart, "whether to restore state on startup") + databaseDSN := flags.StringP("database", "d", config.DatabaseDSN, "PostgreSQL database DSN") err := flags.Parse(args) if err != nil { @@ -126,6 +133,8 @@ func parseFlags(config *Config, progname string, args []string) error { config.StorePath = *storePath case "restore": config.RestoreOnStart = *restoreOnStart + case "database": + config.DatabaseDSN = *databaseDSN } }) @@ -144,6 +153,8 @@ func detectStorageKind(c *Config) string { var sk string switch { + case c.DatabaseDSN != "": + sk = storage.KindDatabase case c.StorePath != "": sk = storage.KindFile default: @@ -159,6 +170,8 @@ func newDataStorage(kind string, config *Config) (storage.MetricsStorage, error) return storage.NewMemStorage(), nil case storage.KindFile: return storage.NewFileStorage(config.StorePath, config.StoreInterval, config.RestoreOnStart) + case storage.KindDatabase: + return storage.NewDatabaseStorage(config.DatabaseDSN) default: return nil, fmt.Errorf("unknown storage type") } diff --git a/internal/storage/database.go b/internal/storage/database.go new file mode 100644 index 0000000..0053aa5 --- /dev/null +++ b/internal/storage/database.go @@ -0,0 +1,136 @@ +package storage + +import ( + "context" + "errors" + "fmt" + + "github.com/ex0rcist/metflix/internal/entities" + "github.com/ex0rcist/metflix/internal/metrics" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgxpool" +) + +var _ MetricsStorage = DatabaseStorage{} + +// implements pgxpool.Pool +type PGXPool interface { + Ping(ctx context.Context) error + Exec(ctx context.Context, sql string, arguments ...any) (pgconn.CommandTag, error) + Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) + QueryRow(ctx context.Context, sql string, args ...any) pgx.Row + Close() +} + +type DatabaseStorage struct { + pool PGXPool +} + +func NewDatabaseStorage(dsn string) (*DatabaseStorage, error) { + migrator := NewMigrator(dsn, "file://db/migrate", 5) + + if err := migrator.Run(); err != nil { + return nil, fmt.Errorf("migrations run failed: %w", err) + } + + pool, err := pgxpool.New(context.Background(), dsn) + if err != nil { + return nil, fmt.Errorf("pgxpool init failed: %w", err) + } + + return &DatabaseStorage{pool: pool}, nil +} + +func (d DatabaseStorage) Push(ctx context.Context, key string, record Record) error { + sql := "INSERT INTO metrics(id, name, kind, value) values ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET value = $4" + _, err := d.pool.Exec(ctx, sql, key, record.Name, record.Value.Kind(), record.Value.String()) + + if err != nil { + return fmt.Errorf("db storage Push() error: %w", err) + } + + return nil +} + +func (d DatabaseStorage) Get(ctx context.Context, key string) (Record, error) { + var ( + name string + kind string + value float64 + record Record + err error + ) + + sql := "SELECT name, kind, value FROM metrics WHERE id=$1" + err = d.pool.QueryRow(ctx, sql, key).Scan(&name, &kind, &value) + + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return Record{}, entities.ErrRecordNotFound + } + + return record, fmt.Errorf("db storage Get() error: %w", err) + } + + switch kind { + case metrics.KindCounter: + record, err = Record{Name: name, Value: metrics.Counter(value)}, nil + case metrics.KindGauge: + record, err = Record{Name: name, Value: metrics.Gauge(value)}, nil + default: + err = fmt.Errorf("db storage kind=%s unknown", kind) + } + + return record, err +} + +func (d DatabaseStorage) List(ctx context.Context) ([]Record, error) { + rows, err := d.pool.Query(ctx, "SELECT name, kind, value FROM metrics") + if err != nil { + return nil, fmt.Errorf("db storage List() error: %w", err) + } + + defer rows.Close() + + var ( + name string + kind string + value float64 + ) + + result := make([]Record, 0) + _, err = pgx.ForEachRow(rows, []any{&name, &kind, &value}, func() error { + switch kind { + case metrics.KindCounter: + result = append(result, Record{Name: name, Value: metrics.Counter(value)}) + return nil + + case metrics.KindGauge: + result = append(result, Record{Name: name, Value: metrics.Gauge(value)}) + return nil + + default: + return fmt.Errorf("db storage kind=%s unknown", kind) + } + }) + + if err != nil { + return nil, fmt.Errorf("db storage List() error: %w", err) + } + + return result, nil +} + +func (d DatabaseStorage) Ping(ctx context.Context) error { + if err := d.pool.Ping(ctx); err != nil { + return fmt.Errorf("db storage Ping() error: %w", err) + } + + return nil +} + +func (d DatabaseStorage) Close(ctx context.Context) error { + d.pool.Close() + return nil +} diff --git a/internal/storage/file.go b/internal/storage/file.go index 78583f0..6895deb 100644 --- a/internal/storage/file.go +++ b/internal/storage/file.go @@ -1,6 +1,7 @@ package storage import ( + "context" "encoding/json" "fmt" "os" @@ -44,8 +45,8 @@ func NewFileStorage(storePath string, storeInterval int, restoreOnStart bool) (* return fs, nil } -func (s *FileStorage) Push(id string, record Record) error { - if err := s.MemStorage.Push(id, record); err != nil { +func (s *FileStorage) Push(ctx context.Context, id string, record Record) error { + if err := s.MemStorage.Push(ctx, id, record); err != nil { return err } @@ -56,7 +57,7 @@ func (s *FileStorage) Push(id string, record Record) error { return nil } -func (s *FileStorage) Close() error { +func (s *FileStorage) Close(_ context.Context) error { if s.dumpTicker != nil { s.dumpTicker.Stop() } diff --git a/internal/storage/file_test.go b/internal/storage/file_test.go index 4da3299..9451f9a 100644 --- a/internal/storage/file_test.go +++ b/internal/storage/file_test.go @@ -1,6 +1,7 @@ package storage import ( + "context" "encoding/json" "os" "testing" @@ -43,6 +44,7 @@ func TestNewFileStorage(t *testing.T) { } func TestSyncPushAndDump(t *testing.T) { + ctx := context.Background() storePath := "test_store.json" defer removeFile(t, storePath) @@ -50,7 +52,7 @@ func TestSyncPushAndDump(t *testing.T) { checkNoError(t, err, "failed to create new FileStorage") record := Record{Name: "test", Value: metrics.Counter(42)} - err = fs.Push(record.CalculateRecordID(), record) + err = fs.Push(ctx, record.CalculateRecordID(), record) checkNoError(t, err, "failed to push record") data, err := os.ReadFile(storePath) @@ -59,12 +61,14 @@ func TestSyncPushAndDump(t *testing.T) { err = json.Unmarshal(data, &fs.MemStorage) checkNoError(t, err, "failed to unmarshal storage file") - if got, err := fs.Get(record.CalculateRecordID()); err != nil || got != record { + if got, err := fs.Get(ctx, record.CalculateRecordID()); err != nil || got != record { t.Errorf("expected record %v, got %v", record, got) } } func TestRestore(t *testing.T) { + ctx := context.Background() + storePath := "test_store.json" defer removeFile(t, storePath) @@ -73,14 +77,14 @@ func TestRestore(t *testing.T) { checkNoError(t, err, "failed to create new FileStorage") record := Record{Name: "test", Value: metrics.Counter(42)} - err = fs1.Push(record.CalculateRecordID(), record) // dumped + err = fs1.Push(ctx, record.CalculateRecordID(), record) // dumped checkNoError(t, err, "failed to push to FileStorage") // new storage from dump fs2, err := NewFileStorage(storePath, 0, true) checkNoError(t, err, "failed to create new FileStorage") - restoredRecord, err := fs2.Get(record.CalculateRecordID()) + restoredRecord, err := fs2.Get(ctx, record.CalculateRecordID()) if err != nil { checkNoError(t, err, "expected to find restored record, but did not") } @@ -90,6 +94,8 @@ func TestRestore(t *testing.T) { } func TestAsyncDumping(t *testing.T) { + ctx := context.Background() + storePath := "test_store.json" defer removeFile(t, storePath) @@ -97,12 +103,12 @@ func TestAsyncDumping(t *testing.T) { checkNoError(t, err, "failed to create new FileStorage") record := Record{Name: "test", Value: metrics.Counter(42)} - err = fs.Push(record.CalculateRecordID(), record) + err = fs.Push(ctx, record.CalculateRecordID(), record) checkNoError(t, err, "failed to push record") time.Sleep(1000 * time.Millisecond) - err = fs.Close() + err = fs.Close(ctx) checkNoError(t, err, "failed to close fs") data, err := os.ReadFile(storePath) @@ -112,7 +118,7 @@ func TestAsyncDumping(t *testing.T) { err = json.Unmarshal(data, &ms) checkNoError(t, err, "failed to unmarshal storage file") - if restored, err := ms.Get(record.CalculateRecordID()); err != nil || restored != record { + if restored, err := ms.Get(ctx, record.CalculateRecordID()); err != nil || restored != record { t.Errorf("expected record %v, got %v", record, restored) } } diff --git a/internal/storage/memory.go b/internal/storage/memory.go index a38b1be..27e0e9e 100644 --- a/internal/storage/memory.go +++ b/internal/storage/memory.go @@ -1,6 +1,8 @@ package storage import ( + "context" + "github.com/ex0rcist/metflix/internal/entities" ) @@ -17,13 +19,13 @@ func NewMemStorage() *MemStorage { } } -func (s *MemStorage) Push(id string, record Record) error { +func (s *MemStorage) Push(_ context.Context, id string, record Record) error { s.Data[id] = record return nil } -func (s *MemStorage) Get(id string) (Record, error) { +func (s *MemStorage) Get(_ context.Context, id string) (Record, error) { record, ok := s.Data[id] if !ok { return Record{}, entities.ErrRecordNotFound @@ -32,7 +34,7 @@ func (s *MemStorage) Get(id string) (Record, error) { return record, nil } -func (s *MemStorage) List() ([]Record, error) { +func (s *MemStorage) List(_ context.Context) ([]Record, error) { arr := make([]Record, len(s.Data)) i := 0 @@ -54,6 +56,6 @@ func (s *MemStorage) Snapshot() *MemStorage { return &MemStorage{Data: snapshot} } -func (s *MemStorage) Close() error { +func (s *MemStorage) Close(_ context.Context) error { return nil // do nothing } diff --git a/internal/storage/memory_test.go b/internal/storage/memory_test.go index 5b60253..c4fe578 100644 --- a/internal/storage/memory_test.go +++ b/internal/storage/memory_test.go @@ -1,6 +1,7 @@ package storage import ( + "context" "testing" "github.com/ex0rcist/metflix/internal/metrics" @@ -8,6 +9,7 @@ import ( ) func TestMemStorage_Push(t *testing.T) { + ctx := context.Background() strg := NewMemStorage() records := []Record{ @@ -18,18 +20,19 @@ func TestMemStorage_Push(t *testing.T) { for _, r := range records { id := r.CalculateRecordID() - err := strg.Push(id, r) + err := strg.Push(ctx, id, r) if err != nil { t.Fatalf("expected no error, got %v", err) } - if s, _ := strg.Get(id); r != s { + if s, _ := strg.Get(ctx, id); r != s { t.Fatalf("expected record %v, got %v", r, s) } } } func TestMemStorage_Push_WithSameName(t *testing.T) { + ctx := context.Background() strg := NewMemStorage() counterValue := metrics.Counter(42) @@ -43,17 +46,17 @@ func TestMemStorage_Push_WithSameName(t *testing.T) { for _, r := range records { id := r.CalculateRecordID() - if err := strg.Push(id, r); err != nil { + if err := strg.Push(ctx, id, r); err != nil { t.Fatalf("expected no error, got %v", err) } } - storedCounter, err := strg.Get(records[0].CalculateRecordID()) + storedCounter, err := strg.Get(ctx, records[0].CalculateRecordID()) if err != nil { t.Fatalf("expected no error, got %v", err) } - storedGauge, err := strg.Get(records[1].CalculateRecordID()) + storedGauge, err := strg.Get(ctx, records[1].CalculateRecordID()) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -68,9 +71,10 @@ func TestMemStorage_Push_WithSameName(t *testing.T) { } func TestMemStorage_Get(t *testing.T) { + ctx := context.Background() strg := NewMemStorage() record := Record{Name: "1", Value: metrics.Counter(42)} - err := strg.Push(record.CalculateRecordID(), record) + err := strg.Push(ctx, record.CalculateRecordID(), record) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -87,7 +91,7 @@ func TestMemStorage_Get(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := strg.Get(tt.id) + got, err := strg.Get(ctx, tt.id) if (err != nil) != tt.wantError { t.Fatalf("expected error: %v, got %v", tt.wantError, err) } @@ -99,6 +103,7 @@ func TestMemStorage_Get(t *testing.T) { } func TestMemStorage_List(t *testing.T) { + ctx := context.Background() storage := NewMemStorage() records := []Record{ @@ -106,17 +111,17 @@ func TestMemStorage_List(t *testing.T) { {Name: metrics.KindCounter, Value: metrics.Counter(42)}, } - err := storage.Push(records[0].CalculateRecordID(), records[0]) + err := storage.Push(ctx, records[0].CalculateRecordID(), records[0]) if err != nil { t.Fatalf("expected no error, got %v", err) } - err = storage.Push(records[1].CalculateRecordID(), records[1]) + err = storage.Push(ctx, records[1].CalculateRecordID(), records[1]) if err != nil { t.Fatalf("expected no error, got %v", err) } - got, err := storage.List() + got, err := storage.List(ctx) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/storage/migrate.go b/internal/storage/migrate.go new file mode 100644 index 0000000..40ea1ff --- /dev/null +++ b/internal/storage/migrate.go @@ -0,0 +1,69 @@ +package storage + +import ( + "errors" + "time" + + "github.com/ex0rcist/metflix/internal/logging" + + "github.com/golang-migrate/migrate/v4" + _ "github.com/golang-migrate/migrate/v4/database/postgres" + _ "github.com/golang-migrate/migrate/v4/source/file" +) + +type Migrator struct { + dsn string + retries int + source string + + migrator *migrate.Migrate + err error +} + +func NewMigrator(dsn string, source string, retries int) Migrator { + return Migrator{dsn: dsn, source: source, retries: retries} +} + +func (m Migrator) Run() error { + for m.retries > 0 { + logging.LogInfo("migrations: connecting to " + m.dsn) + + m.migrator, m.err = migrate.New(m.source, m.dsn) + if m.err == nil { + break + } + + m.retries-- + time.Sleep(time.Second) + } + + if m.err != nil { + return m.err + } + + m.err = m.migrator.Up() + + defer func() { + srcErr, dbErr := m.migrator.Close() + + if srcErr != nil { + logging.LogError(srcErr, "failed closing migrator", srcErr.Error()) + } + + if dbErr != nil { + logging.LogError(dbErr, "failed closing migrator", dbErr.Error()) + } + }() + + if m.err == nil { + logging.LogInfo("migrations: success") + return nil + } + + if errors.Is(m.err, migrate.ErrNoChange) { + logging.LogInfo("migrations: no change") + return nil + } + + return m.err +} diff --git a/internal/storage/pgx_mock.go b/internal/storage/pgx_mock.go new file mode 100644 index 0000000..83c9054 --- /dev/null +++ b/internal/storage/pgx_mock.go @@ -0,0 +1,43 @@ +package storage + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/stretchr/testify/mock" +) + +var _ PGXPool = (*PGXPoolMock)(nil) + +type PGXPoolMock struct { + mock.Mock +} + +func NewPGXPoolMock() *PGXPoolMock { + return new(PGXPoolMock) +} + +func (m *PGXPoolMock) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) { + mArgs := m.Called(ctx) + return mArgs.Get(0).(pgconn.CommandTag), mArgs.Error(1) +} + +func (m *PGXPoolMock) Ping(ctx context.Context) error { + args := m.Called(ctx) + return args.Error(0) +} + +func (m *PGXPoolMock) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) { + mArgs := m.Called(ctx, sql, args) + return mArgs.Get(0).(pgx.Rows), mArgs.Error(1) +} + +func (m *PGXPoolMock) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row { + mArgs := m.Called(ctx, sql, args) + return mArgs.Get(0).(pgx.Row) +} + +func (m *PGXPoolMock) Close() { + _ = m.Called() +} diff --git a/internal/storage/pinger.go b/internal/storage/pinger.go new file mode 100644 index 0000000..204eaf1 --- /dev/null +++ b/internal/storage/pinger.go @@ -0,0 +1,39 @@ +package storage + +import ( + "context" + "fmt" + "time" + + "github.com/ex0rcist/metflix/internal/entities" +) + +var _ Pinger = PingerService{} + +type Pinger interface { + Ping(ctx context.Context) error +} + +type PingerService struct { + storage MetricsStorage +} + +func NewPingerService(storage MetricsStorage) PingerService { + return PingerService{storage: storage} +} + +func (s PingerService) Ping(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + strg, ok := s.storage.(Pinger) + if !ok { + return fmt.Errorf("storage ping failed: %w", entities.ErrStorageUnpingable) + } + + if err := strg.Ping(ctx); err != nil { + return fmt.Errorf("storage check failed: %w", err) + } + + return nil +} diff --git a/internal/storage/pinger_mock.go b/internal/storage/pinger_mock.go new file mode 100644 index 0000000..b6b870d --- /dev/null +++ b/internal/storage/pinger_mock.go @@ -0,0 +1,18 @@ +package storage + +import ( + "context" + + "github.com/stretchr/testify/mock" +) + +var _ Pinger = (*PingerMock)(nil) + +type PingerMock struct { + mock.Mock +} + +func (m *PingerMock) Ping(ctx context.Context) error { + args := m.Called(ctx) + return args.Error(0) +} diff --git a/internal/storage/pinger_test.go b/internal/storage/pinger_test.go new file mode 100644 index 0000000..a68fdb7 --- /dev/null +++ b/internal/storage/pinger_test.go @@ -0,0 +1,46 @@ +package storage + +import ( + "context" + "errors" + "testing" + + "github.com/ex0rcist/metflix/internal/entities" + "github.com/stretchr/testify/mock" +) + +func TestPing(t *testing.T) { + tests := []struct { + name string + err error + }{ + {name: "should return nil if storage online"}, + {name: "should return error if storage offline", err: entities.ErrUnexpected}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pm := NewPGXPoolMock() + pm.On("Ping", mock.Anything).Return(tt.err) + + store := &DatabaseStorage{pool: pm} + pinger := NewPingerService(store) + + err := pinger.Ping(context.Background()) + if !errors.Is(err, tt.err) { + t.Fatalf("expected error to be %v, got: %v", tt.err, err) + } + + }) + } +} + +func TestPingOnUnpingableStorage(t *testing.T) { + store := NewMemStorage() + pinger := NewPingerService(store) + + err := pinger.Ping(context.Background()) + if !errors.Is(err, entities.ErrStorageUnpingable) { + t.Fatalf("expected error to be %v, got %v", entities.ErrStorageUnpingable, err) + } +} diff --git a/internal/storage/service.go b/internal/storage/service.go index 12eeee7..796e450 100644 --- a/internal/storage/service.go +++ b/internal/storage/service.go @@ -1,6 +1,7 @@ package storage import ( + "context" "errors" "sort" @@ -11,26 +12,26 @@ import ( // Common interface for service layer // Storage service prepares data before calling storage type StorageService interface { - List() ([]Record, error) - Push(record Record) (Record, error) - Get(name, kind string) (Record, error) + List(ctx context.Context) ([]Record, error) + Push(ctx context.Context, record Record) (Record, error) + Get(ctx context.Context, name, kind string) (Record, error) } // ensure Service implements StorageService var _ StorageService = Service{} type Service struct { - storage MetricsStorage + Storage MetricsStorage } func NewService(storage MetricsStorage) Service { - return Service{storage: storage} + return Service{Storage: storage} } -func (s Service) Get(name, kind string) (Record, error) { +func (s Service) Get(ctx context.Context, name, kind string) (Record, error) { id := CalculateRecordID(name, kind) - record, err := s.storage.Get(id) + record, err := s.Storage.Get(ctx, id) if err != nil { return Record{}, err } @@ -38,14 +39,14 @@ func (s Service) Get(name, kind string) (Record, error) { return record, nil } -func (s Service) Push(record Record) (Record, error) { - newValue, err := s.calculateNewValue(record) +func (s Service) Push(ctx context.Context, record Record) (Record, error) { + newValue, err := s.calculateNewValue(ctx, record) if err != nil { return Record{}, err } record.Value = newValue - err = s.storage.Push(record.CalculateRecordID(), record) + err = s.Storage.Push(ctx, record.CalculateRecordID(), record) if err != nil { return Record{}, err @@ -54,8 +55,8 @@ func (s Service) Push(record Record) (Record, error) { return record, nil } -func (s Service) List() ([]Record, error) { - records, err := s.storage.List() +func (s Service) List(ctx context.Context) ([]Record, error) { + records, err := s.Storage.List(ctx) if err != nil { return nil, err } @@ -67,7 +68,7 @@ func (s Service) List() ([]Record, error) { return records, nil } -func (s Service) calculateNewValue(record Record) (metrics.Metric, error) { +func (s Service) calculateNewValue(ctx context.Context, record Record) (metrics.Metric, error) { if record.Value.Kind() != metrics.KindCounter { return record.Value, nil } @@ -77,7 +78,7 @@ func (s Service) calculateNewValue(record Record) (metrics.Metric, error) { return record.Value, entities.ErrMetricMissingName } - storedRecord, err := s.storage.Get(id) + storedRecord, err := s.Storage.Get(ctx, id) if errors.Is(err, entities.ErrRecordNotFound) { return record.Value, nil } else if err != nil { diff --git a/internal/storage/service_mock.go b/internal/storage/service_mock.go index e693bcd..4dfdcde 100644 --- a/internal/storage/service_mock.go +++ b/internal/storage/service_mock.go @@ -1,6 +1,8 @@ package storage import ( + "context" + "github.com/stretchr/testify/mock" ) @@ -11,17 +13,17 @@ type ServiceMock struct { mock.Mock } -func (m *ServiceMock) Get(name, kind string) (Record, error) { +func (m *ServiceMock) Get(ctx context.Context, name, kind string) (Record, error) { args := m.Called(name, kind) return args.Get(0).(Record), args.Error(1) } -func (m *ServiceMock) Push(record Record) (Record, error) { +func (m *ServiceMock) Push(ctx context.Context, record Record) (Record, error) { args := m.Called(record) return args.Get(0).(Record), args.Error(1) } -func (m *ServiceMock) List() ([]Record, error) { +func (m *ServiceMock) List(ctx context.Context) ([]Record, error) { args := m.Called() if args.Get(0) == nil { diff --git a/internal/storage/service_test.go b/internal/storage/service_test.go index 2d72ff8..fc8adf6 100644 --- a/internal/storage/service_test.go +++ b/internal/storage/service_test.go @@ -1,6 +1,7 @@ package storage import ( + "context" "testing" "github.com/ex0rcist/metflix/internal/entities" @@ -24,7 +25,7 @@ func TestService_Get(t *testing.T) { { name: "existing record", mock: func(m *StorageMock) { - m.On("Get", "test_counter").Return(Record{Name: "test", Value: metrics.Counter(42)}, nil) + m.On("Get", mock.Anything, "test_counter").Return(Record{Name: "test", Value: metrics.Counter(42)}, nil) }, args: args{name: "test", kind: metrics.KindCounter}, @@ -35,7 +36,7 @@ func TestService_Get(t *testing.T) { { name: "non-existing record", mock: func(m *StorageMock) { - m.On("Get", "test_counter").Return(Record{}, entities.ErrRecordNotFound) + m.On("Get", mock.Anything, "test_counter").Return(Record{}, entities.ErrRecordNotFound) }, args: args{name: "test", kind: metrics.KindCounter}, expected: Record{}, @@ -43,6 +44,7 @@ func TestService_Get(t *testing.T) { }, } + ctx := context.Background() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := new(StorageMock) @@ -52,7 +54,7 @@ func TestService_Get(t *testing.T) { tt.mock(m) } - result, err := service.Get(tt.args.name, tt.args.kind) + result, err := service.Get(ctx, tt.args.name, tt.args.kind) if (err != nil) != tt.wantErr { t.Fatalf("expected error: %v, got %v", tt.wantErr, err) } @@ -78,8 +80,8 @@ func TestService_Push(t *testing.T) { mock: func(m *StorageMock) { r := Record{Name: "test", Value: metrics.Counter(42)} - m.On("Get", "test_counter").Return(Record{}, entities.ErrRecordNotFound) - m.On("Push", "test_counter", r).Return(nil) // no error, successful push + m.On("Get", mock.Anything, "test_counter").Return(Record{}, entities.ErrRecordNotFound) + m.On("Push", mock.Anything, "test_counter", r).Return(nil) // no error, successful push }, record: Record{Name: "test", Value: metrics.Counter(42)}, expected: Record{Name: "test", Value: metrics.Counter(42)}, @@ -91,8 +93,8 @@ func TestService_Push(t *testing.T) { oldr := Record{Name: "test", Value: metrics.Counter(42)} newr := Record{Name: "test", Value: metrics.Counter(84)} - m.On("Get", "test_counter").Return(oldr, nil) - m.On("Push", "test_counter", newr).Return(nil) // no error, successful push + m.On("Get", mock.Anything, "test_counter").Return(oldr, nil) + m.On("Push", mock.Anything, "test_counter", newr).Return(nil) // no error, successful push }, record: Record{Name: "test", Value: metrics.Counter(42)}, expected: Record{Name: "test", Value: metrics.Counter(84)}, @@ -103,8 +105,8 @@ func TestService_Push(t *testing.T) { mock: func(m *StorageMock) { r := Record{Name: "test", Value: metrics.Gauge(42.42)} - m.On("Get", "test_gauge").Return(Record{}, entities.ErrRecordNotFound) - m.On("Push", "test_gauge", r).Return(nil) // no error, successful push + m.On("Get", mock.Anything, "test_gauge").Return(Record{}, entities.ErrRecordNotFound) + m.On("Push", mock.Anything, "test_gauge", r).Return(nil) // no error, successful push }, record: Record{Name: "test", Value: metrics.Gauge(42.42)}, expected: Record{Name: "test", Value: metrics.Gauge(42.42)}, @@ -116,8 +118,8 @@ func TestService_Push(t *testing.T) { oldr := Record{Name: "test", Value: metrics.Gauge(42.42)} newr := Record{Name: "test", Value: metrics.Gauge(43.43)} - m.On("Get", "test_gauge").Return(oldr, nil) - m.On("Push", "test_gauge", newr).Return(nil) // no error, successful push + m.On("Get", mock.Anything, "test_gauge").Return(oldr, nil) + m.On("Push", mock.Anything, "test_gauge", newr).Return(nil) // no error, successful push }, record: Record{Name: "test", Value: metrics.Gauge(43.43)}, expected: Record{Name: "test", Value: metrics.Gauge(43.43)}, @@ -126,7 +128,7 @@ func TestService_Push(t *testing.T) { { name: "underlying error", mock: func(m *StorageMock) { - m.On("Push", "test_gauge", mock.AnythingOfType("Record")).Return(entities.ErrUnexpected) + m.On("Push", mock.Anything, "test_gauge", mock.AnythingOfType("Record")).Return(entities.ErrUnexpected) }, record: Record{Name: "test", Value: metrics.Gauge(43.43)}, expected: Record{}, @@ -141,7 +143,7 @@ func TestService_Push(t *testing.T) { { name: "storage get error", mock: func(m *StorageMock) { - m.On("Get", "test_counter").Return(Record{}, entities.ErrUnexpected) + m.On("Get", mock.Anything, "test_counter").Return(Record{}, entities.ErrUnexpected) }, record: Record{Name: "test", Value: metrics.Counter(43)}, expected: Record{}, @@ -149,6 +151,7 @@ func TestService_Push(t *testing.T) { }, } + ctx := context.Background() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := new(StorageMock) @@ -158,7 +161,7 @@ func TestService_Push(t *testing.T) { tt.mock(m) } - result, err := service.Push(tt.record) + result, err := service.Push(ctx, tt.record) if (err != nil) != tt.wantErr { t.Fatalf("expected error: %v, got %v", tt.wantErr, err) } @@ -179,7 +182,7 @@ func TestService_List(t *testing.T) { { name: "normal list", mock: func(m *StorageMock) { - m.On("List").Return([]Record{ + m.On("List", mock.Anything).Return([]Record{ {Name: "metricX", Value: metrics.Counter(42)}, {Name: "metricA", Value: metrics.Gauge(42.42)}, }, nil) @@ -194,13 +197,14 @@ func TestService_List(t *testing.T) { { name: "had error", mock: func(m *StorageMock) { - m.On("List").Return([]Record{}, entities.ErrUnexpected) + m.On("List", mock.Anything).Return([]Record{}, entities.ErrUnexpected) }, expected: []Record{}, wantErr: true, }, } + ctx := context.Background() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := new(StorageMock) @@ -210,7 +214,7 @@ func TestService_List(t *testing.T) { tt.mock(m) } - result, err := service.List() + result, err := service.List(ctx) if (err != nil) != tt.wantErr { t.Fatalf("expected error: %v, got %v", tt.wantErr, err) } @@ -241,7 +245,7 @@ func TestService_calculateNewValue(t *testing.T) { { name: "new counter record", mock: func(m *StorageMock) { - m.On("Get", "test_counter").Return(Record{}, entities.ErrRecordNotFound) + m.On("Get", mock.Anything, "test_counter").Return(Record{}, entities.ErrRecordNotFound) }, record: Record{Name: "test", Value: metrics.Counter(42)}, expected: metrics.Counter(42), @@ -250,7 +254,7 @@ func TestService_calculateNewValue(t *testing.T) { { name: "existing counter record", mock: func(m *StorageMock) { - m.On("Get", "test_counter").Return(Record{Name: "test", Value: metrics.Counter(42)}, nil) + m.On("Get", mock.Anything, "test_counter").Return(Record{Name: "test", Value: metrics.Counter(42)}, nil) }, record: Record{Name: "test", Value: metrics.Counter(42)}, expected: metrics.Counter(84), @@ -259,7 +263,7 @@ func TestService_calculateNewValue(t *testing.T) { { name: "new gauge record", mock: func(m *StorageMock) { - m.On("Get", "test_gauge").Return(Record{}, entities.ErrRecordNotFound) + m.On("Get", mock.Anything, "test_gauge").Return(Record{}, entities.ErrRecordNotFound) }, record: Record{Name: "test", Value: metrics.Gauge(42.42)}, expected: metrics.Gauge(42.42), @@ -268,7 +272,7 @@ func TestService_calculateNewValue(t *testing.T) { { name: "existing gauge record", mock: func(m *StorageMock) { - m.On("Get", "test_gauge").Return(Record{Name: "test", Value: metrics.Gauge(42.42)}) + m.On("Get", mock.Anything, "test_gauge").Return(Record{Name: "test", Value: metrics.Gauge(42.42)}) }, record: Record{Name: "test", Value: metrics.Gauge(43.43)}, expected: metrics.Gauge(43.43), @@ -277,7 +281,7 @@ func TestService_calculateNewValue(t *testing.T) { { name: "underlying error", mock: func(m *StorageMock) { - m.On("Get", "test_counter").Return(Record{}, entities.ErrUnexpected) + m.On("Get", mock.Anything, "test_counter").Return(Record{}, entities.ErrUnexpected) }, record: Record{Name: "test", Value: metrics.Counter(42)}, expected: nil, @@ -285,6 +289,7 @@ func TestService_calculateNewValue(t *testing.T) { }, } + ctx := context.Background() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -295,7 +300,7 @@ func TestService_calculateNewValue(t *testing.T) { tt.mock(m) } - result, err := service.calculateNewValue(tt.record) + result, err := service.calculateNewValue(ctx, tt.record) if (err != nil) != tt.wantErr { t.Fatalf("expected error: %v, got %v", tt.wantErr, err) } diff --git a/internal/storage/storage.go b/internal/storage/storage.go index e6f7ee4..b7fde4b 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -1,15 +1,17 @@ package storage +import "context" + const ( - KindMemory = "memory" - KindFile = "file" - KindMock = "mock" + KindMemory = "memory" + KindFile = "file" + KindDatabase = "database" ) // common interface for storages: mem, file, etc type MetricsStorage interface { - Push(id string, record Record) error - Get(id string) (Record, error) - List() ([]Record, error) - Close() error + Push(ctx context.Context, id string, record Record) error + Get(ctx context.Context, id string) (Record, error) + List(ctx context.Context) ([]Record, error) + Close(ctx context.Context) error } diff --git a/internal/storage/storage_mock.go b/internal/storage/storage_mock.go index 2853eee..81d4567 100644 --- a/internal/storage/storage_mock.go +++ b/internal/storage/storage_mock.go @@ -1,6 +1,8 @@ package storage import ( + "context" + "github.com/stretchr/testify/mock" ) @@ -11,18 +13,18 @@ type StorageMock struct { mock.Mock } -func (m *StorageMock) Get(id string) (Record, error) { - args := m.Called(id) +func (m *StorageMock) Get(ctx context.Context, id string) (Record, error) { + args := m.Called(ctx, id) return args.Get(0).(Record), args.Error(1) } -func (m *StorageMock) Push(id string, record Record) error { - args := m.Called(id, record) +func (m *StorageMock) Push(ctx context.Context, id string, record Record) error { + args := m.Called(ctx, id, record) return args.Error(0) } -func (m *StorageMock) List() ([]Record, error) { - args := m.Called() +func (m *StorageMock) List(ctx context.Context) ([]Record, error) { + args := m.Called(ctx) if args.Get(0) == nil { return nil, args.Error(1) @@ -31,8 +33,8 @@ func (m *StorageMock) List() ([]Record, error) { return args.Get(0).([]Record), args.Error(1) } -func (m *StorageMock) Close() error { - args := m.Called() +func (m *StorageMock) Close(ctx context.Context) error { + args := m.Called(ctx) return args.Error(0) }