ae9cc3ecb55e392503f56d8f7b01c3a4f424e2d6

Author
Ayman Bagabas <ayman.bagabas@gmail.com>
Committer
Ayman Bagabas <ayman.bagabas@gmail.com>
Date

Message

feat(server): add prometheus stats server

Diff

This diff is truncated to protect this page.

  1diff --git a/go.mod b/go.mod
  2index ea287b8a72ecb35b94c9858d32a5504ade2c8c1e..a21aed2bca8fd2d0af6aee82874a61d37b86f15a 100644
  3--- a/go.mod
  4+++ b/go.mod
  5@@ -4,7 +4,6 @@ go 1.17
  6 
  7 require (
  8 	github.com/alecthomas/chroma v0.10.0
  9-	github.com/caarlos0/env/v6 v6.10.1
 10 	github.com/charmbracelet/bubbles v0.15.0
 11 	github.com/charmbracelet/bubbletea v0.23.2
 12 	github.com/charmbracelet/glamour v0.6.0
 13@@ -20,6 +19,7 @@ require (
 14 
 15 require (
 16 	github.com/aymanbagabas/go-osc52 v1.2.2
 17+	github.com/caarlos0/env/v7 v7.1.0
 18 	github.com/charmbracelet/keygen v0.3.0
 19 	github.com/charmbracelet/log v0.2.1
 20 	github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103
 21@@ -28,6 +28,8 @@ require (
 22 	github.com/lrstanley/bubblezone v0.0.0-20220716194435-3cb8c52f6a8f
 23 	github.com/muesli/mango-cobra v1.2.0
 24 	github.com/muesli/roff v0.1.0
 25+	github.com/prometheus/client_golang v1.14.0
 26+	github.com/robfig/cron/v3 v3.0.1
 27 	github.com/spf13/cobra v1.6.1
 28 	goji.io v2.0.2+incompatible
 29 	golang.org/x/crypto v0.7.0
 30@@ -40,16 +42,20 @@ require (
 31 	github.com/atotto/clipboard v0.1.4 // indirect
 32 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
 33 	github.com/aymerick/douceur v0.2.0 // indirect
 34+	github.com/beorn7/perks v1.0.1 // indirect
 35 	github.com/caarlos0/sshmarshal v0.1.0 // indirect
 36+	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 37 	github.com/containerd/console v1.0.3 // indirect
 38 	github.com/dlclark/regexp2 v1.4.0 // indirect
 39 	github.com/go-logfmt/logfmt v0.6.0 // indirect
 40+	github.com/golang/protobuf v1.5.2 // indirect
 41 	github.com/gorilla/css v1.0.0 // indirect
 42 	github.com/inconshreveable/mousetrap v1.0.1 // indirect
 43 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 44 	github.com/mattn/go-isatty v0.0.18 // indirect
 45 	github.com/mattn/go-localereader v0.0.1 // indirect
 46 	github.com/mattn/go-runewidth v0.0.14 // indirect
 47+	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
 48 	github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 // indirect
 49 	github.com/microcosm-cc/bluemonday v1.0.21 // indirect
 50 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 51@@ -58,6 +64,9 @@ require (
 52 	github.com/muesli/mango v0.1.0 // indirect
 53 	github.com/muesli/mango-pflag v0.1.0 // indirect
 54 	github.com/olekukonko/tablewriter v0.0.5 // indirect
 55+	github.com/prometheus/client_model v0.3.0 // indirect
 56+	github.com/prometheus/common v0.37.0 // indirect
 57+	github.com/prometheus/procfs v0.8.0 // indirect
 58 	github.com/rivo/uniseg v0.2.0 // indirect
 59 	github.com/sahilm/fuzzy v0.1.0 // indirect
 60 	github.com/spf13/pflag v1.0.5 // indirect
 61@@ -67,4 +76,5 @@ require (
 62 	golang.org/x/sys v0.6.0 // indirect
 63 	golang.org/x/term v0.6.0 // indirect
 64 	golang.org/x/text v0.8.0 // indirect
 65+	google.golang.org/protobuf v1.28.1 // indirect
 66 )
 67diff --git a/go.sum b/go.sum
 68index 04e8a16f6787ffec10cbf2d570ba0392ee91e189..8587ada7d55d021c2bf620476510d3c4d68d2f0e 100644
 69--- a/go.sum
 70+++ b/go.sum
 71@@ -1,8 +1,48 @@
 72+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 73+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 74+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
 75+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
 76+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
 77+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
 78+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
 79+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
 80+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
 81+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
 82+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
 83+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
 84+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
 85+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
 86+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
 87+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 88+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
 89+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
 90+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
 91+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
 92+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
 93+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 94+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
 95+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 96+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
 97+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
 98+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
 99+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
100+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
101+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
102+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
103+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
104+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
105+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
106+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
107 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
108 github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
109 github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
110 github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
111 github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
112+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
113+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
114+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
115+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
116+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
117 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
118 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
119 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
120@@ -16,12 +56,20 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
121 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
122 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
123 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
124+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
125+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
126+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
127+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
128 github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
129-github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II=
130-github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
131+github.com/caarlos0/env/v7 v7.1.0 h1:9lzTF5amyQeWHZzuZeKlCb5FWSUxpG1js43mhbY8ozg=
132+github.com/caarlos0/env/v7 v7.1.0/go.mod h1:LPPWniDUq4JaO6Q41vtlyikhMknqymCLBw0eX4dcH1E=
133 github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA=
134 github.com/caarlos0/sshmarshal v0.1.0 h1:zTCZrDORFfWh526Tsb7vCm3+Yg/SfW/Ub8aQDeosk0I=
135 github.com/caarlos0/sshmarshal v0.1.0/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA=
136+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
137+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
138+github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
139+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
140 github.com/charmbracelet/bubbles v0.15.0 h1:c5vZ3woHV5W2b8YZI1q7v4ZNQaPetfHuoHzx+56Z6TI=
141 github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74=
142 github.com/charmbracelet/bubbletea v0.22.0/go.mod h1:aoVIwlNlr5wbCB26KhxfrqAn0bMp4YpJcoOelbxApjs=
143@@ -44,7 +92,12 @@ github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103 h1:wpHMERIN0pQZE
144 github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103/go.mod h1:0Vm2/8yBljiLDnGJHU8ehswfawrEybGk33j5ssqKQVM=
145 github.com/charmbracelet/wish v1.1.0 h1:0ArX9SOG70saqd23NYjoS56oLPVNgqcQegkz1Lw+4zY=
146 github.com/charmbracelet/wish v1.1.0/go.mod h1:yHbm0hs/qX4lFE7nrhAcXjFYc8bxMIfSqJOfOYfwyYo=
147+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
148+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
149+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
150+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
151 github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
152+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
153 github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
154 github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
155 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
156@@ -57,6 +110,10 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
157 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
158 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
159 github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
160+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
161+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
162+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
163+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
164 github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
165 github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
166 github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
167@@ -64,23 +121,106 @@ github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw4
168 github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
169 github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk=
170 github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8=
171diff --git a/server/config/config.go b/server/config/config.go
172index 6a3d5610cdb2182c93d02607ea9f67d98a81cc10..9c8954de383fb36c1d13b77979a13256b69c510f 100644
173--- a/server/config/config.go
174+++ b/server/config/config.go
175@@ -4,7 +4,7 @@ import (
176 	"os"
177 	"path/filepath"
178 
179-	"github.com/caarlos0/env/v6"
180+	"github.com/caarlos0/env/v7"
181 	"github.com/charmbracelet/log"
182 	"github.com/charmbracelet/soft-serve/server/backend"
183 	"gopkg.in/yaml.v3"
184@@ -61,6 +61,12 @@ type HTTPConfig struct {
185 	PublicURL string `env:"PUBLIC_URL" yaml:"public_url"`
186 }
187 
188+// StatsConfig is the configuration for the stats server.
189+type StatsConfig struct {
190+	// ListenAddr is the address on which the stats server will listen.
191+	ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"`
192+}
193+
194 // Config is the configuration for Soft Serve.
195 type Config struct {
196 	// Name is the name of the server.
197@@ -75,6 +81,9 @@ type Config struct {
198 	// HTTP is the configuration for the HTTP server.
199 	HTTP HTTPConfig `envPrefix:"HTTP_" yaml:"http"`
200 
201+	// Stats is the configuration for the stats server.
202+	Stats StatsConfig `envPrefix:"STATS_" yaml:"stats"`
203+
204 	// InitialAdminKeys is a list of public keys that will be added to the list of admins.
205 	InitialAdminKeys []string `env:"INITIAL_ADMIN_KEY" envSeparator:"\n" yaml:"initial_admin_keys"`
206 
207@@ -92,7 +101,7 @@ func ParseConfig(path string) (*Config, error) {
208 	if err != nil {
209 		return nil, err
210 	}
211-	defer f.Close()
212+	defer f.Close() // nolint: errcheck
213 	if err := yaml.NewDecoder(f).Decode(cfg); err != nil {
214 		return nil, err
215 	}
216@@ -129,11 +138,14 @@ func DefaultConfig() *Config {
217 			ListenAddr: ":8080",
218 			PublicURL:  "http://localhost:8080",
219 		},
220+		Stats: StatsConfig{
221+			ListenAddr: ":8081",
222+		},
223 	}
224 	cp := filepath.Join(cfg.DataPath, "config.yaml")
225 	f, err := os.Open(cp)
226 	if err == nil {
227-		defer f.Close()
228+		defer f.Close() // nolint: errcheck
229 		if err := yaml.NewDecoder(f).Decode(cfg); err != nil {
230 			log.Error("failed to decode config", "err", err)
231 		}
232diff --git a/server/config/file.go b/server/config/file.go
233index 932b8063700bb17ceb19649583530f02c596ae4c..8f295cf6abfddcf9d1b403a7ecdebe1cd9583d3b 100644
234--- a/server/config/file.go
235+++ b/server/config/file.go
236@@ -64,6 +64,11 @@ http:
237   # This is the address will be used to clone repositories.
238   public_url: "{{ .HTTP.PublicURL }}"
239 
240+# The stats server configuration.
241+stats:
242+  # The address on which the stats server will listen.
243+  listen_addr: "{{ .Stats.ListenAddr }}"
244+
245 `))
246 )
247 
248diff --git a/server/daemon.go b/server/daemon.go
249index 6424988d75d361ab9b715526942face540ce00d5..047bf4b6cd8bf4b5d69836893e469e0deb9e909e 100644
250--- a/server/daemon.go
251+++ b/server/daemon.go
252@@ -13,6 +13,24 @@ import (
253 	"github.com/charmbracelet/soft-serve/server/config"
254 	"github.com/charmbracelet/soft-serve/server/utils"
255 	"github.com/go-git/go-git/v5/plumbing/format/pktline"
256+	"github.com/prometheus/client_golang/prometheus"
257+	"github.com/prometheus/client_golang/prometheus/promauto"
258+)
259+
260+var (
261+	uploadPackGitCounter = promauto.NewCounterVec(prometheus.CounterOpts{
262+		Namespace: "soft_serve",
263+		Subsystem: "git",
264+		Name:      "git_upload_pack_total",
265+		Help:      "The total number of git-upload-pack requests",
266+	}, []string{"repo"})
267+
268+	uploadArchiveGitCounter = promauto.NewCounterVec(prometheus.CounterOpts{
269+		Namespace: "soft_serve",
270+		Subsystem: "git",
271+		Name:      "git_upload_archive_total",
272+		Help:      "The total number of git-upload-archive requests",
273+	}, []string{"repo"})
274 )
275 
276 // ErrServerClosed indicates that the server has been closed.
277@@ -184,12 +202,14 @@ func (d *GitDaemon) handleClient(conn net.Conn) {
278 		}
279 
280 		gitPack := uploadPack
281+		counter := uploadPackGitCounter
282 		cmd := string(split[0])
283 		switch cmd {
284 		case uploadPackBin:
285 			gitPack = uploadPack
286 		case uploadArchiveBin:
287 			gitPack = uploadArchive
288+			counter = uploadArchiveGitCounter
289 		default:
290 			fatal(c, ErrInvalidRequest)
291 			return
292@@ -223,6 +243,8 @@ func (d *GitDaemon) handleClient(conn net.Conn) {
293 			fatal(c, err)
294 			return
295 		}
296+
297+		counter.WithLabelValues(name)
298 	}
299 }
300 
301diff --git a/server/http.go b/server/http.go
302index 2ac99b5e497db0ceb1d954a0a23801caa7d5f733..30f4547d9a3c44ff68d275e40020a2c3e12aeb23 100644
303--- a/server/http.go
304+++ b/server/http.go
305@@ -15,11 +15,29 @@ import (
306 	"github.com/charmbracelet/soft-serve/server/config"
307 	"github.com/charmbracelet/soft-serve/server/utils"
308 	"github.com/dustin/go-humanize"
309+	"github.com/prometheus/client_golang/prometheus"
310+	"github.com/prometheus/client_golang/prometheus/promauto"
311 	"goji.io"
312 	"goji.io/pat"
313 	"goji.io/pattern"
314 )
315 
316+var (
317+	gitHttpCounter = promauto.NewCounterVec(prometheus.CounterOpts{
318+		Namespace: "soft_serve",
319+		Subsystem: "http",
320+		Name:      "git_fetch_pull_total",
321+		Help:      "The total number of git fetch/pull requests",
322+	}, []string{"repo", "file"})
323+
324+	goGetCounter = promauto.NewCounterVec(prometheus.CounterOpts{
325+		Namespace: "soft_serve",
326+		Subsystem: "http",
327+		Name:      "go_get_total",
328+		Help:      "The total number of go get requests",
329+	}, []string{"repo"})
330+)
331+
332 // logWriter is a wrapper around http.ResponseWriter that allows us to capture
333 // the HTTP status code and bytes written to the response.
334 type logWriter struct {
335@@ -179,12 +197,12 @@ var (
336 		return findStringSubmatch(u.Path, getLooseObjectRegexp)
337 	}
338 
339-	getPackFileRegexp = regexp.MustCompile(".*?(/objects/pack/pack-[0-9a-f]{40}\\.pack)$")
340+	getPackFileRegexp = regexp.MustCompile(`.*?(/objects/pack/pack-[0-9a-f]{40}\.pack)$`)
341 	getPackFile       = func(u *url.URL) *Match {
342 		return findStringSubmatch(u.Path, getPackFileRegexp)
343 	}
344 
345-	getIdxFileRegexp = regexp.MustCompile(".*?(/objects/pack/pack-[0-9a-f]{40}\\.idx)$")
346+	getIdxFileRegexp = regexp.MustCompile(`.*?(/objects/pack/pack-[0-9a-f]{40}\.idx)$`)
347 	getIdxFile       = func(u *url.URL) *Match {
348 		return findStringSubmatch(u.Path, getIdxFileRegexp)
349 	}
350@@ -264,6 +282,8 @@ func (s *HTTPServer) handleIndex(w http.ResponseWriter, r *http.Request) {
351 		http.Error(w, err.Error(), http.StatusInternalServerError)
352 		return
353 	}
354+
355+	goGetCounter.WithLabelValues(repo).Inc()
356 }
357 
358 func (s *HTTPServer) handleGit(w http.ResponseWriter, r *http.Request) {
359@@ -287,6 +307,7 @@ func (s *HTTPServer) handleGit(w http.ResponseWriter, r *http.Request) {
360 	}
361 
362 	file := pat.Param(r, "file")
363+	gitHttpCounter.WithLabelValues(repo, file).Inc()
364 	r.URL.Path = fmt.Sprintf("/%s/%s", repo, file)
365 	s.dirHandler.ServeHTTP(w, r)
366 }
367diff --git a/server/server.go b/server/server.go
368index eb62692f1f77d465987f613a75cd9422e668d6d3..976b1b9fa7423976936ccf5fc03cd4c48ee6ab79 100644
369--- a/server/server.go
370+++ b/server/server.go
371@@ -21,11 +21,12 @@ var (
372 
373 // Server is the Soft Serve server.
374 type Server struct {
375-	SSHServer  *SSHServer
376-	GitDaemon  *GitDaemon
377-	HTTPServer *HTTPServer
378-	Config     *config.Config
379-	Backend    backend.Backend
380+	SSHServer   *SSHServer
381+	GitDaemon   *GitDaemon
382+	HTTPServer  *HTTPServer
383+	StatsServer *StatsServer
384+	Config      *config.Config
385+	Backend     backend.Backend
386 }
387 
388 // NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
389@@ -74,6 +75,11 @@ func NewServer(cfg *config.Config) (*Server, error) {
390 		return nil, err
391 	}
392 
393+	srv.StatsServer, err = NewStatsServer(cfg)
394+	if err != nil {
395+		return nil, err
396+	}
397+
398 	return srv, nil
399 }
400 
401@@ -101,6 +107,13 @@ func (s *Server) Start() error {
402 		}
403 		return nil
404 	})
405+	errg.Go(func() error {
406+		log.Print("Starting Stats server", "addr", s.Config.Stats.ListenAddr)
407+		if err := s.StatsServer.ListenAndServe(); err != http.ErrServerClosed {
408+			return err
409+		}
410+		return nil
411+	})
412 	return errg.Wait()
413 }
414 
415@@ -116,6 +129,9 @@ func (s *Server) Shutdown(ctx context.Context) error {
416 	errg.Go(func() error {
417 		return s.SSHServer.Shutdown(ctx)
418 	})
419+	errg.Go(func() error {
420+		return s.StatsServer.Shutdown(ctx)
421+	})
422 	return errg.Wait()
423 }
424 
425@@ -125,5 +141,6 @@ func (s *Server) Close() error {
426 	errg.Go(s.GitDaemon.Close)
427 	errg.Go(s.HTTPServer.Close)
428 	errg.Go(s.SSHServer.Close)
429+	errg.Go(s.StatsServer.Close)
430 	return errg.Wait()
431 }
432diff --git a/server/session.go b/server/session.go
433index 03df433ddb530e753e083af6c55f4c4f96d9c044..6c1a655c5bd6a6588d2308affd659e0332a968cd 100644
434--- a/server/session.go
435+++ b/server/session.go
436@@ -13,15 +13,28 @@ import (
437 	"github.com/charmbracelet/ssh"
438 	"github.com/charmbracelet/wish"
439 	bm "github.com/charmbracelet/wish/bubbletea"
440+	"github.com/prometheus/client_golang/prometheus"
441+	"github.com/prometheus/client_golang/prometheus/promauto"
442+)
443+
444+var (
445+	tuiSessionCounter = promauto.NewCounterVec(prometheus.CounterOpts{
446+		Namespace: "soft_serve",
447+		Subsystem: "ssh",
448+		Name:      "tui_session_total",
449+		Help:      "The total number of TUI sessions",
450+	}, []string{"key", "user", "repo", "term"})
451 )
452 
453 // SessionHandler is the soft-serve bubbletea ssh session handler.
454 func SessionHandler(cfg *config.Config) bm.ProgramHandler {
455 	return func(s ssh.Session) *tea.Program {
456+		ak := backend.MarshalAuthorizedKey(s.PublicKey())
457 		pty, _, active := s.Pty()
458 		if !active {
459 			return nil
460 		}
461+
462 		cmd := s.Command()
463 		initialRepo := ""
464 		if len(cmd) == 1 {
465@@ -32,6 +45,7 @@ func SessionHandler(cfg *config.Config) bm.ProgramHandler {
466 				return nil
467 			}
468 		}
469+
470 		envs := s.Environ()
471 		envs = append(envs, fmt.Sprintf("TERM=%s", pty.Term))
472 		output := osc52.NewOutput(s, envs)
473@@ -45,6 +59,9 @@ func SessionHandler(cfg *config.Config) bm.ProgramHandler {
474 			tea.WithoutCatchPanics(),
475 			tea.WithMouseCellMotion(),
476 		)
477+
478+		tuiSessionCounter.WithLabelValues(ak, s.User(), initialRepo, pty.Term).Inc()
479+
480 		return p
481 	}
482 }
483diff --git a/server/ssh.go b/server/ssh.go
484index 6a73e935ac43b0a25872cfb9831e61c311e98112..64f48a00f12144fb49704b2f38ca79e33bfdcd03 100644
485--- a/server/ssh.go
486+++ b/server/ssh.go
487@@ -5,6 +5,7 @@ import (
488 	"errors"
489 	"net"
490 	"path/filepath"
491+	"strconv"
492 	"strings"
493 	"time"
494 
495@@ -20,9 +21,55 @@ import (
496 	lm "github.com/charmbracelet/wish/logging"
497 	rm "github.com/charmbracelet/wish/recover"
498 	"github.com/muesli/termenv"
499+	"github.com/prometheus/client_golang/prometheus"
500+	"github.com/prometheus/client_golang/prometheus/promauto"
501 	gossh "golang.org/x/crypto/ssh"
502 )
503 
504+var (
505+	publicKeyCounter = promauto.NewCounterVec(prometheus.CounterOpts{
506+		Namespace: "soft_serve",
507+		Subsystem: "ssh",
508+		Name:      "public_key_auth_total",
509+		Help:      "The total number of public key auth requests",
510+	}, []string{"key", "user", "access", "allowed"})
511+
512+	keyboardInteractiveCounter = promauto.NewCounterVec(prometheus.CounterOpts{
513+		Namespace: "soft_serve",
514+		Subsystem: "ssh",
515+		Name:      "keyboard_interactive_auth_total",
516+		Help:      "The total number of keyboard interactive auth requests",
517+	}, []string{"user", "allowed"})
518+
519+	uploadPackCounter = promauto.NewCounterVec(prometheus.CounterOpts{
520+		Namespace: "soft_serve",
521+		Subsystem: "ssh",
522+		Name:      "git_upload_pack_total",
523+		Help:      "The total number of git-upload-pack requests",
524+	}, []string{"key", "user", "repo"})
525+
526+	receivePackCounter = promauto.NewCounterVec(prometheus.CounterOpts{
527+		Namespace: "soft_serve",
528+		Subsystem: "ssh",
529+		Name:      "git_receive_pack_total",
530+		Help:      "The total number of git-receive-pack requests",
531+	}, []string{"key", "user", "repo"})
532+
533+	uploadArchiveCounter = promauto.NewCounterVec(prometheus.CounterOpts{
534+		Namespace: "soft_serve",
535+		Subsystem: "ssh",
536+		Name:      "git_upload_archive_total",
537+		Help:      "The total number of git-upload-archive requests",
538+	}, []string{"key", "user", "repo"})
539+
540+	createRepoCounter = promauto.NewCounterVec(prometheus.CounterOpts{
541+		Namespace: "soft_serve",
542+		Subsystem: "ssh",
543+		Name:      "create_repo_total",
544+		Help:      "The total number of create repo requests",
545+	}, []string{"key", "user", "repo"})
546+)
547+
548 // SSHServer is a SSH server that implements the git protocol.
549 type SSHServer struct {
550 	srv *ssh.Server
551@@ -90,12 +137,18 @@ func (s *SSHServer) Shutdown(ctx context.Context) error {
552 
553 // PublicKeyAuthHandler handles public key authentication.
554 func (s *SSHServer) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) bool {
555-	return s.cfg.Backend.AccessLevel("", pk) >= backend.ReadOnlyAccess
556+	ac := s.cfg.Backend.AccessLevel("", pk)
557+	allowed := ac >= backend.ReadOnlyAccess
558+	ak := backend.MarshalAuthorizedKey(pk)
559+	publicKeyCounter.WithLabelValues(ak, ctx.User(), ac.String(), strconv.FormatBool(allowed)).Inc()
560+	return allowed
561 }
562 
563 // KeyboardInteractiveHandler handles keyboard interactive authentication.
564 func (s *SSHServer) KeyboardInteractiveHandler(ctx ssh.Context, _ gossh.KeyboardInteractiveChallenge) bool {
565-	return s.cfg.Backend.AllowKeyless() && s.PublicKeyHandler(ctx, nil)
566+	ac := s.cfg.Backend.AllowKeyless() && s.PublicKeyHandler(ctx, nil)
567+	keyboardInteractiveCounter.WithLabelValues(ctx.User(), strconv.FormatBool(ac)).Inc()
568+	return ac
569 }
570 
571 // Middleware adds Git server functionality to the ssh.Server. Repos are stored
572@@ -113,6 +166,7 @@ func (s *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
573 					// repo should be in the form of "repo.git"
574 					name := utils.SanitizeRepo(cmd[1])
575 					pk := s.PublicKey()
576+					ak := backend.MarshalAuthorizedKey(pk)
577 					access := cfg.Backend.AccessLevel(name, pk)
578 					// git bare repositories should end in ".git"
579 					// https://git-scm.com/docs/gitrepository-layout
580@@ -136,26 +190,34 @@ func (s *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
581 								sshFatal(s, err)
582 								return
583 							}
584+							createRepoCounter.WithLabelValues(ak, s.User(), name).Inc()
585 						}
586 						if err := receivePack(s, s, s.Stderr(), repoDir); err != nil {
587diff --git a/server/stats.go b/server/stats.go
588new file mode 100644
589index 0000000000000000000000000000000000000000..30b8cd0bef9ba125c293b7bd7a6d5b72e882bee0
590--- /dev/null
591+++ b/server/stats.go
592@@ -0,0 +1,48 @@
593+package server
594+
595+import (
596+	"context"
597+	"net/http"
598+	"time"
599+
600+	"github.com/charmbracelet/soft-serve/server/config"
601+	"github.com/prometheus/client_golang/prometheus/promhttp"
602+)
603+
604+// StatsServer is a server for collecting and reporting statistics.
605+type StatsServer struct {
606+	cfg    *config.Config
607+	server *http.Server
608+}
609+
610+// NewStatsServer returns a new StatsServer.
611+func NewStatsServer(cfg *config.Config) (*StatsServer, error) {
612+	mux := http.NewServeMux()
613+	mux.Handle("/metrics", promhttp.Handler())
614+	return &StatsServer{
615+		cfg: cfg,
616+		server: &http.Server{
617+			Addr:              cfg.Stats.ListenAddr,
618+			Handler:           mux,
619+			ReadHeaderTimeout: time.Second * 10,
620+			ReadTimeout:       time.Second * 10,
621+			WriteTimeout:      time.Second * 10,
622+			MaxHeaderBytes:    http.DefaultMaxHeaderBytes,
623+		},
624+	}, nil
625+}
626+
627+// ListenAndServe starts the StatsServer.
628+func (s *StatsServer) ListenAndServe() error {
629+	return s.server.ListenAndServe()
630+}
631+
632+// Shutdown gracefully shuts down the StatsServer.
633+func (s *StatsServer) Shutdown(ctx context.Context) error {
634+	return s.server.Shutdown(ctx)
635+}
636+
637+// Close closes the StatsServer.
638+func (s *StatsServer) Close() error {
639+	return s.server.Close()
640+}