приложение собрано каркасом и вшито в бинарник
- заведён каталог web/ — Vue 3, роутер пятой версии, сборка Vite; собранное вшивается через go:embed и раздаётся корневым маршрутом: разметка на неизвестном пути вне корней сервиса, отказ контракта внутри корня - перечень корней сервиса стал единой точкой и порождает регистрацию маршрутов, а не описывает её; журнал раздачи пишет исход и длину пути, но не сам путь - шаг front зовёт Node контейнером docker — Biome, юнит-тесты Vue и сборка входят в гейт, а в Dockerfile появилась ступень приложения
This commit is contained in:
@@ -23,6 +23,7 @@ import (
|
||||
"git.vakhrushev.me/av/transcriber/internal/controller/worker"
|
||||
"git.vakhrushev.me/av/transcriber/internal/metrics"
|
||||
"git.vakhrushev.me/av/transcriber/internal/service"
|
||||
"git.vakhrushev.me/av/transcriber/web"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
@@ -167,6 +168,15 @@ func main() {
|
||||
SecureCookie: cfg.Auth.SecureCookie,
|
||||
}, logger)
|
||||
|
||||
// Адресное пространство сервиса объявлено одним перечнем, и он порождает
|
||||
// регистрацию, а не описывает её: корень, заведённый мимо перечня, не
|
||||
// получит обработчика вовсе. Отсюда же уровень журнала для адресов
|
||||
// наблюдения и правило неизвестного пути у раздачи приложения.
|
||||
mounts := httpcontroller.ServiceMounts(appHandler, authHandler, promhttp.Handler())
|
||||
|
||||
dist, appBuilt := web.Dist()
|
||||
webappHandler := httpcontroller.NewWebappHandler(dist, appBuilt, mounts, logger)
|
||||
|
||||
// Сервер приезжает каналом, а не общей переменной: хук исполняется в
|
||||
// горутине сервера, а читает его горутина остановки, и связи «произошло
|
||||
// раньше» между ними иначе нет.
|
||||
@@ -186,17 +196,29 @@ func main() {
|
||||
err := e.Next()
|
||||
|
||||
level := slog.LevelInfo
|
||||
if e.Request.URL.Path == "/health" || e.Request.URL.Path == "/metrics" {
|
||||
if httpcontroller.IsObservationAddress(mounts, e.Request.URL.Path) {
|
||||
// Опрос здоровья и метрик идёт постоянно и полезного не несёт.
|
||||
level = slog.LevelDebug
|
||||
}
|
||||
|
||||
logger.Log(e.Request.Context(), level, "Incoming request",
|
||||
attrs := []any{
|
||||
"http.method", e.Request.Method,
|
||||
"http.route", journalRoute(e.Request.URL.Path),
|
||||
"http.route", journalRoute(e.Request.URL.Path, mounts),
|
||||
"http.status_code", e.Status(),
|
||||
"duration_ms", time.Since(start).Milliseconds(),
|
||||
"transport", "http")
|
||||
"transport", "http",
|
||||
}
|
||||
|
||||
// Путь, отданный приложению, в журнал не идёт — вместо него исход
|
||||
// и длина: по ним видно, что происходит, а множеством значений
|
||||
// самого пути распоряжается спрашивающий.
|
||||
if outcome := httpcontroller.WebappOutcome(e); outcome != "" {
|
||||
attrs = append(attrs,
|
||||
"webapp.outcome", outcome,
|
||||
"http.path_length", len(e.Request.URL.Path))
|
||||
}
|
||||
|
||||
logger.Log(e.Request.Context(), level, "Incoming request", attrs...)
|
||||
|
||||
return err
|
||||
})
|
||||
@@ -222,20 +244,11 @@ func main() {
|
||||
return fmt.Errorf("failed to apply app rate limit: %w", err)
|
||||
}
|
||||
|
||||
authHandler.Register(se.Router)
|
||||
appHandler.Register(se.Router)
|
||||
httpcontroller.RegisterServiceRoutes(se.Router, mounts)
|
||||
|
||||
se.Router.GET("/health", func(e *core.RequestEvent) error {
|
||||
return e.JSON(http.StatusOK, map[string]string{
|
||||
"status": "ok",
|
||||
"message": "Transcriber service is running",
|
||||
})
|
||||
})
|
||||
|
||||
se.Router.GET("/metrics", func(e *core.RequestEvent) error {
|
||||
promhttp.Handler().ServeHTTP(e.Response, e.Request)
|
||||
return nil
|
||||
})
|
||||
// Раздача приложения вешается последней: она занимает корень, и всё,
|
||||
// что не совпало ни с одним адресом сервиса, доходит до неё.
|
||||
webappHandler.Register(se.Router)
|
||||
|
||||
return se.Next()
|
||||
})
|
||||
@@ -313,6 +326,9 @@ func main() {
|
||||
// сегмент такого пути и есть имя файла в хранилище.
|
||||
const filesPathPrefix = "/api/files/"
|
||||
|
||||
// webappRoute — чем в журнале обозначается всякий путь, отданный приложению.
|
||||
const webappRoute = "<приложение>"
|
||||
|
||||
// journalRoute готовит путь запроса к записи в журнал.
|
||||
//
|
||||
// Инвариант проекта запрещает имени файла в хранилище попадать в журнал: имя —
|
||||
@@ -323,7 +339,16 @@ const filesPathPrefix = "/api/files/"
|
||||
//
|
||||
// Срезается только имя: маршрут остаётся различимым, и наблюдаемость от этого не
|
||||
// теряется.
|
||||
func journalRoute(path string) string {
|
||||
//
|
||||
// Путь, не принадлежащий сервису, в журнал не идёт вовсе. До появления раздачи
|
||||
// приложения такой путь ловил отказ маршрутизатора, а теперь получает разметку
|
||||
// с кодом `200`: множеством его значений распоряжается спрашивающий, и
|
||||
// дословная строка сделала бы журнал местом, куда аноним пишет свой текст.
|
||||
func journalRoute(path string, mounts []httpcontroller.Mount) string {
|
||||
if !httpcontroller.IsServiceAddress(mounts, path) {
|
||||
return webappRoute
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(path, filesPathPrefix) {
|
||||
return path
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user