package http import ( "net/http" "strings" ) // Отдельные адреса наблюдения. Корней у сервиса остался один — корень // приложения; `/api` и `/_` ушли вместе со встроенным хранилищем и его панелью, // и адресов под этими именами не существует. // // Резервировать имя за отказом сервис не берётся: имя, за которым ничего не // стоит, ничем не отличается от любого другого свободного, а второй перечень // «когда-то занятых корней» разошёлся бы с первым молча. const ( HealthPath = "/health" MetricsPath = "/metrics" ) // Mount — часть адресного пространства, принадлежащая сервису. // // Перечень этих частей — **единственное** описание того, что сервису // принадлежит, и он не описывает регистрацию, а порождает её: корень, // заведённый мимо перечня, не получит обработчика вовсе. Из него же выводятся // правило неизвестного пути, уровень журнала и область действия узнавания. type Mount struct { // Path — корень либо точный адрес. Path string // Exact — путь является точным адресом, а не корнем: `/health` накрывает // только сам себя, а `/app` — всё, что под ним. Exact bool // Bind вешает обработчики этой части. Bind func(mux *http.ServeMux) } // Covers говорит, принадлежит ли путь этой части адресного пространства. // // Условий два, и оба обязательны: точное совпадение либо префикс **вместе с // косой чертой**. По одному префиксу корню `/app` достался бы посторонний // `/apple`; по одному префиксу с косой чертой голый `/app` не достался бы никому // и уехал бы разметкой приложения. func (m Mount) Covers(requestPath string) bool { if m.Exact { return requestPath == m.Path } return requestPath == m.Path || strings.HasPrefix(requestPath, m.Path+"/") } // ServiceMounts перечисляет адресное пространство сервиса целиком. // // Обработчик приложения приходит уже одетым в свои слои — ограничитель частоты, // узнавание, требование учётной записи: область их действия и есть корень // приложения, и берётся она отсюда, а не перечисляется вторым списком. func ServiceMounts(app http.Handler, metricsHandler http.Handler) []Mount { return []Mount{ {Path: AppRoot, Bind: bindApp(app)}, {Path: HealthPath, Exact: true, Bind: bindHealth}, {Path: MetricsPath, Exact: true, Bind: bindMetrics(metricsHandler)}, } } // RegisterServiceRoutes вешает всё, что сервис вешает сам. func RegisterServiceRoutes(mux *http.ServeMux, mounts []Mount) { for _, mount := range mounts { if mount.Bind != nil { mount.Bind(mux) } } } // ExactAddressOf называет точный адрес сервиса, которому отвечает путь. Второе // значение ложно у всего прочего — у пути под корнем и у пути вне корней. // // Точные адреса — закрытый перечень, и только они пишутся в журнал дословно: // значением такого пути распоряжается не спрашивающий, а сам перечень. func ExactAddressOf(mounts []Mount, requestPath string) (string, bool) { for _, mount := range mounts { if mount.Exact && mount.Covers(requestPath) { return mount.Path, true } } return "", false } // IsObservationAddress говорит, что путь — адрес наблюдения. // // Опрос здоровья и метрик идёт постоянно и полезного не несёт, поэтому уровень // журнала у него свой. Перечень при этом тот же самый: второе перечисление этих // адресов разошлось бы с первым молча. func IsObservationAddress(mounts []Mount, requestPath string) bool { _, ok := ExactAddressOf(mounts, requestPath) return ok } // bindApp вешает корень приложения. // // Образцов два, и оба обязательны: без точного `/app` маршрутизатор увёл бы // голый корень перенаправлением на `/app/`, а норма требует от него отказа // приложения, а не переезда. func bindApp(app http.Handler) func(mux *http.ServeMux) { return func(mux *http.ServeMux) { mux.Handle(AppRoot+"/", app) mux.Handle(AppRoot, app) } } func bindHealth(mux *http.ServeMux) { mux.HandleFunc(HealthPath, func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]string{ "status": "ok", "message": "Transcriber service is running", }) }) } func bindMetrics(handler http.Handler) func(mux *http.ServeMux) { return func(mux *http.ServeMux) { mux.Handle(MetricsPath, handler) } }