diff --git a/api/go.mod b/api/go.mod index e671d27..880d94a 100644 --- a/api/go.mod +++ b/api/go.mod @@ -6,16 +6,25 @@ require ( github.com/julsemaan/anyfile-notepad/utils v0.0.0-20230202010526-481b7f9b59a2 github.com/julsemaan/rest-layer-file v0.0.0-20230518012330-1c28ed9eb6a7 github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/prometheus/client_golang v1.23.2 github.com/rs/rest-layer v0.0.0-20160505213648-cb84bc79b5b8 - gopkg.in/alexcesaro/statsd.v2 v2.0.0 ) require ( - github.com/rs/cors v1.9.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/evanphx/json-patch v4.1.0+incompatible // indirect + github.com/kr/text v0.2.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/rs/xid v1.5.0 // indirect - github.com/stretchr/testify v1.8.2 // indirect - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect - golang.org/x/net v0.10.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect ) // Uncomment this to use local directory for utils diff --git a/api/go.sum b/api/go.sum index b676885..b409ab3 100644 --- a/api/go.sum +++ b/api/go.sum @@ -1,17 +1,42 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/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/evanphx/json-patch v4.1.0+incompatible h1:K1MDoo4AZ4wU0GIU/fPmtZg7VpzLjCxu+UwBD1FvwOc= github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/graphql-go/graphql v0.7.6/go.mod h1:k6yrAYQaSP59DC5UVxbgxESlmVyojThKdORUqGDGmrI= -github.com/julsemaan/anyfile-notepad/utils v0.0.0-20230202010526-481b7f9b59a2 h1:nVLPZUH4mD5okHdVlRfQltQZj9De3Gjx8RCBBSi26GQ= -github.com/julsemaan/anyfile-notepad/utils v0.0.0-20230202010526-481b7f9b59a2/go.mod h1:zJsqF8VPdI8dtbIMpRwGC6Q2cSBaKLz71nY4C+Msf5g= github.com/julsemaan/rest-layer-file v0.0.0-20230518012330-1c28ed9eb6a7 h1:Wb2nBwi5//IkrXZk2wCq6yN1jWPJPGtZILAbqEv86yE= github.com/julsemaan/rest-layer-file v0.0.0-20230518012330-1c28ed9eb6a7/go.mod h1:cTD+gl7oqD1MpcQuS2LReenHy7aoccUHvzPaLknlcpk= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= @@ -24,31 +49,34 @@ github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -56,30 +84,28 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 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/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= -gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7SLonnE4drU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 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/api/internal/app/config.go b/api/internal/app/config.go index 8ccc958..76ac2df 100644 --- a/api/internal/app/config.go +++ b/api/internal/app/config.go @@ -7,16 +7,17 @@ import ( const defaultDataDir = "./db" const defaultListenAddr = ":8080" +const defaultMetricsListenAddr = "127.0.0.1:9090" const defaultContactRequestsPerDay = 10 const envMaxContactRequestsPerDay = "AFN_MAX_CONTACT_REQUESTS_PER_DAY" type Config struct { DataDir string ListenAddr string + MetricsListenAddr string Username string Password string SupportEmail string - StatsdAddress string MaxContactRequestsPerDay int } @@ -31,13 +32,18 @@ func LoadConfigFromEnv() Config { listenAddr = defaultListenAddr } + metricsListenAddr := os.Getenv("AFN_METRICS_LISTEN_ADDR") + if metricsListenAddr == "" { + metricsListenAddr = defaultMetricsListenAddr + } + return Config{ DataDir: dataDir, ListenAddr: listenAddr, + MetricsListenAddr: metricsListenAddr, Username: os.Getenv("AFN_REST_USERNAME"), Password: os.Getenv("AFN_REST_PASSWORD"), SupportEmail: os.Getenv("AFN_SUPPORT_EMAIL"), - StatsdAddress: os.Getenv("AFN_STATSD_URI"), MaxContactRequestsPerDay: loadMaxContactRequestsPerDay(), } } diff --git a/api/internal/app/config_test.go b/api/internal/app/config_test.go index d25335f..8e78bf5 100644 --- a/api/internal/app/config_test.go +++ b/api/internal/app/config_test.go @@ -3,7 +3,25 @@ package app import "testing" func TestLoadConfigFromEnv(t *testing.T) { - t.Run("uses default max contact requests when env missing", func(t *testing.T) { + t.Run("uses default metrics listen addr when env is empty", func(t *testing.T) { + t.Setenv("AFN_METRICS_LISTEN_ADDR", "") + + cfg := LoadConfigFromEnv() + if cfg.MetricsListenAddr != defaultMetricsListenAddr { + t.Fatalf("expected default metrics listen addr, got %q", cfg.MetricsListenAddr) + } + }) + + t.Run("uses configured metrics listen addr when provided", func(t *testing.T) { + t.Setenv("AFN_METRICS_LISTEN_ADDR", ":9191") + + cfg := LoadConfigFromEnv() + if cfg.MetricsListenAddr != ":9191" { + t.Fatalf("expected configured metrics listen addr, got %q", cfg.MetricsListenAddr) + } + }) + + t.Run("uses default max contact requests when env is empty", func(t *testing.T) { t.Setenv(envMaxContactRequestsPerDay, "") cfg := LoadConfigFromEnv() diff --git a/api/internal/app/run.go b/api/internal/app/run.go index 6403817..46ed99a 100644 --- a/api/internal/app/run.go +++ b/api/internal/app/run.go @@ -1,38 +1,28 @@ package app import ( + "errors" "log" "net/http" + "strings" "time" "github.com/julsemaan/anyfile-notepad/api/internal/contact" "github.com/julsemaan/anyfile-notepad/api/internal/httpapi" - "github.com/julsemaan/anyfile-notepad/api/internal/logging" "github.com/julsemaan/anyfile-notepad/api/internal/resources" "github.com/julsemaan/anyfile-notepad/api/internal/stats" cache "github.com/patrickmn/go-cache" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/rest-layer/resource" "github.com/rs/rest-layer/rest" "github.com/rs/rest-layer/schema" - "gopkg.in/alexcesaro/statsd.v2" ) func Run(cfg Config) error { schema.CreatedField.ReadOnly = false schema.UpdatedField.ReadOnly = false - statsConn, err := statsd.New(statsd.Address(cfg.StatsdAddress)) - if err != nil { - logging.Errorf("statsd initialization failed: %v", err) - } - if statsConn != nil { - defer statsConn.Close() - } - - var metrics stats.Metrics - if statsConn != nil { - metrics = statsConn - } + metrics := stats.NewPrometheusMetrics() statsService := stats.NewService(metrics) contactCache := cache.New(24*time.Hour, time.Minute) contactService := contact.NewService(contactCache, cfg.MaxContactRequestsPerDay, cfg.SupportEmail, sendEmailWithOptionalTLS) @@ -54,6 +44,52 @@ func Run(cfg Config) error { cfg.Password, ) - log.Printf("Serving API on http://localhost%s", cfg.ListenAddr) - return http.ListenAndServe(cfg.ListenAddr, router) + metricsMux := http.NewServeMux() + metricsMux.Handle("/metrics", promhttp.Handler()) + + metricsServer := &http.Server{ + Addr: cfg.MetricsListenAddr, + Handler: metricsMux, + ReadHeaderTimeout: 5 * time.Second, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 60 * time.Second, + } + + apiServer := &http.Server{ + Addr: cfg.ListenAddr, + Handler: router, + ReadHeaderTimeout: 5 * time.Second, + ReadTimeout: 10 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 60 * time.Second, + } + + errCh := make(chan error, 2) + go func() { + log.Printf("Serving Prometheus metrics on %s", logURL(cfg.MetricsListenAddr, "/metrics")) + errCh <- metricsServer.ListenAndServe() + }() + + go func() { + log.Printf("Serving API on %s", logURL(cfg.ListenAddr, "")) + errCh <- apiServer.ListenAndServe() + }() + + for { + err := <-errCh + if errors.Is(err, http.ErrServerClosed) { + continue + } + + return err + } +} + +func logURL(addr string, path string) string { + if strings.HasPrefix(addr, ":") { + addr = "localhost" + addr + } + + return "http://" + addr + path } diff --git a/api/internal/app/run_test.go b/api/internal/app/run_test.go new file mode 100644 index 0000000..3171375 --- /dev/null +++ b/api/internal/app/run_test.go @@ -0,0 +1,19 @@ +package app + +import "testing" + +func TestLogURL(t *testing.T) { + t.Run("adds localhost for port only address", func(t *testing.T) { + got := logURL(":8080", "") + if got != "http://localhost:8080" { + t.Fatalf("expected localhost url, got %q", got) + } + }) + + t.Run("uses host qualified address as is", func(t *testing.T) { + got := logURL("0.0.0.0:9090", "/metrics") + if got != "http://0.0.0.0:9090/metrics" { + t.Fatalf("expected host-qualified url, got %q", got) + } + }) +} diff --git a/api/internal/httpapi/router_test.go b/api/internal/httpapi/router_test.go index b4e6252..024c8ea 100644 --- a/api/internal/httpapi/router_test.go +++ b/api/internal/httpapi/router_test.go @@ -18,7 +18,6 @@ func TestRouter(t *testing.T) { statsCalled = true w.WriteHeader(http.StatusOK) }) - router := NewRouter(apiHandler, statsHandler, "user", "password") t.Run("stats route bypasses auth", func(t *testing.T) { diff --git a/api/internal/stats/prometheus.go b/api/internal/stats/prometheus.go new file mode 100644 index 0000000..c102e0d --- /dev/null +++ b/api/internal/stats/prometheus.go @@ -0,0 +1,33 @@ +package stats + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var statsHitsCounter = promauto.NewCounter(prometheus.CounterOpts{ + Name: "afn_stats_hits_total", + Help: "Number of accepted stats payloads.", +}) + +var statsIncrementCounter = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "afn_stats_increment_total", + Help: "Number of accepted increment stats by key.", + }, + []string{"key"}, +) + +type PrometheusMetrics struct{} + +func NewPrometheusMetrics() *PrometheusMetrics { + return &PrometheusMetrics{} +} + +func (m *PrometheusMetrics) IncrementStatsHits() { + statsHitsCounter.Inc() +} + +func (m *PrometheusMetrics) IncrementKey(key string) { + statsIncrementCounter.WithLabelValues(key).Inc() +} diff --git a/api/internal/stats/service.go b/api/internal/stats/service.go index 46d4897..3efd8aa 100644 --- a/api/internal/stats/service.go +++ b/api/internal/stats/service.go @@ -15,13 +15,38 @@ var ErrInvalidJSON = errors.New("invalid json") var ErrPayloadTooLarge = errors.New("payload too large") const maxPayloadSizeBytes = 4 * 1024 -const unknownIPMetricKey = "unknown" +const metricKeyOther = "other" var remoteAddrRegex = regexp.MustCompile(`^([0-9.]+):`) var metricKeyRegex = regexp.MustCompile(`^[a-zA-Z0-9_.-]{1,64}$`) +var allowedIncrementMetricKeys = map[string]struct{}{ + "afn.app.app-load": {}, + "afn.app.mobile-device": {}, + "afn.app.dev-mode": {}, + "afn.app.try-dev-mode": {}, + "afn.app.try-dev-mode-forced": {}, + "afn.app.stop-dev-mode": {}, + "afn.app.stop-dev-mode-forced": {}, + "afn.app.file-print": {}, + "afn.app.GoogleOAuthController.prototype.show_reauth": {}, +} + +type metricKeyPrefixBucket struct { + prefix string + bucket string +} + +var incrementMetricKeyPrefixBuckets = []metricKeyPrefixBucket{ + {prefix: "afn.app.file-edit.extensions", bucket: "afn.app.file-edit.*"}, + {prefix: "afn.app.setting-syntax-manually.", bucket: "afn.app.setting-syntax-manually.*"}, + {prefix: "afn.app.file-edit.", bucket: "afn.app.file-edit.*"}, + {prefix: "afn.app.file-update.", bucket: "afn.app.file-update.*"}, +} + type Metrics interface { - Increment(bucket string) + IncrementStatsHits() + IncrementKey(key string) } type Service struct { @@ -69,39 +94,27 @@ func (s *Service) Record(payload map[string]string) { return } - ipKey := normalizeIPMetricKey(payload["ip"]) - s.metrics.Increment("afn.stats-hits." + ipKey) + s.metrics.IncrementStatsHits() if payload["type"] == "increment" { if metricKeyRegex.MatchString(payload["key"]) { - s.metrics.Increment(payload["key"]) + s.metrics.IncrementKey(normalizeIncrementMetricKey(payload["key"])) } } } -func normalizeIPMetricKey(raw string) string { - candidate := strings.TrimSpace(raw) - if candidate == "" { - return unknownIPMetricKey +func normalizeIncrementMetricKey(key string) string { + if _, ok := allowedIncrementMetricKeys[key]; ok { + return key } - if host, _, err := net.SplitHostPort(candidate); err == nil { - candidate = host - } else { - candidate = strings.Trim(candidate, "[]") - } - - ip := net.ParseIP(candidate) - if ip == nil { - return unknownIPMetricKey - } - - ipKey := strings.NewReplacer(".", "_", ":", "_").Replace(ip.String()) - if ipKey == "" || len(ipKey) > 64 { - return unknownIPMetricKey + for _, bucket := range incrementMetricKeyPrefixBuckets { + if strings.HasPrefix(key, bucket.prefix) { + return bucket.bucket + } } - return ipKey + return metricKeyOther } func extractIP(r *http.Request) string { diff --git a/api/internal/stats/service_test.go b/api/internal/stats/service_test.go index c58ce09..ee34ed0 100644 --- a/api/internal/stats/service_test.go +++ b/api/internal/stats/service_test.go @@ -21,11 +21,16 @@ func (errReader) Close() error { } type metricsStub struct { + hits int keys []string } -func (s *metricsStub) Increment(bucket string) { - s.keys = append(s.keys, bucket) +func (s *metricsStub) IncrementStatsHits() { + s.hits++ +} + +func (s *metricsStub) IncrementKey(key string) { + s.keys = append(s.keys, key) } func TestParsePayload(t *testing.T) { @@ -103,28 +108,66 @@ func TestRecord(t *testing.T) { stub := &metricsStub{} svc := NewService(stub) - svc.Record(map[string]string{"ip": "192.0.2.15", "type": "increment", "key": "hits"}) + svc.Record(map[string]string{"ip": "192.0.2.15", "type": "increment", "key": "afn.app.app-load"}) - expected := []string{"afn.stats-hits.192_0_2_15", "hits"} + if stub.hits != 1 { + t.Fatalf("expected 1 stats hit increment, got %d", stub.hits) + } + + expected := []string{"afn.app.app-load"} if !reflect.DeepEqual(stub.keys, expected) { t.Fatalf("unexpected metrics keys: %#v", stub.keys) } + stub.hits = 0 + stub.keys = nil + svc.Record(map[string]string{"ip": "192.0.2.15", "type": "increment", "key": "afn.app.file-edit.extensions.txt"}) + if stub.hits != 1 { + t.Fatalf("expected 1 stats hit increment, got %d", stub.hits) + } + expected = []string{"afn.app.file-edit.*"} + if !reflect.DeepEqual(stub.keys, expected) { + t.Fatalf("unexpected metrics keys for extension bucket: %#v", stub.keys) + } + + stub.hits = 0 + stub.keys = nil + svc.Record(map[string]string{"ip": "192.0.2.15", "type": "increment", "key": "afn.app.some-new-stat"}) + if stub.hits != 1 { + t.Fatalf("expected 1 stats hit increment, got %d", stub.hits) + } + expected = []string{metricKeyOther} + if !reflect.DeepEqual(stub.keys, expected) { + t.Fatalf("unexpected metrics keys for fallback bucket: %#v", stub.keys) + } + + stub.hits = 0 stub.keys = nil svc.Record(map[string]string{"ip": "2001:db8::1"}) - if len(stub.keys) != 1 || stub.keys[0] != "afn.stats-hits.2001_db8__1" { - t.Fatalf("expected sanitized ipv6 metric key, got %#v", stub.keys) + if stub.hits != 1 { + t.Fatalf("expected 1 stats hit increment, got %d", stub.hits) + } + if len(stub.keys) != 0 { + t.Fatalf("did not expect increment key metric, got %#v", stub.keys) } + stub.hits = 0 stub.keys = nil svc.Record(map[string]string{"ip": "192.0.2.15", "type": "increment", "key": "bad:key"}) - if len(stub.keys) != 1 || stub.keys[0] != "afn.stats-hits.192_0_2_15" { + if stub.hits != 1 { + t.Fatalf("expected 1 stats hit increment, got %d", stub.hits) + } + if len(stub.keys) != 0 { t.Fatalf("expected invalid metric key to be ignored, got %#v", stub.keys) } + stub.hits = 0 stub.keys = nil svc.Record(map[string]string{"ip": "not-an-ip"}) - if len(stub.keys) != 1 || stub.keys[0] != "afn.stats-hits.unknown" { - t.Fatalf("expected invalid ip metric key to be normalized, got %#v", stub.keys) + if stub.hits != 1 { + t.Fatalf("expected 1 stats hit increment, got %d", stub.hits) + } + if len(stub.keys) != 0 { + t.Fatalf("did not expect increment key metric, got %#v", stub.keys) } } diff --git a/docker4dev/docker-compose.yaml b/docker4dev/docker-compose.yaml index 952b5c4..252dd6d 100644 --- a/docker4dev/docker-compose.yaml +++ b/docker4dev/docker-compose.yaml @@ -30,12 +30,14 @@ services: target: build ports: - 8001:8080 + - 8003:9090 env_file: - .env environment: AFN_REST_DATA_DIR: /var/www/api AFN_REST_USERNAME: deez AFN_REST_PASSWORD: nuts + AFN_METRICS_LISTEN_ADDR: :9090 volumes: - api:/var/www/api - ../api/:/src/api/