forked from templates/template-go-backend
new proxy
This commit is contained in:
19
.gitignore
vendored
19
.gitignore
vendored
@@ -1,17 +1,12 @@
|
||||
# игнорируем содержимое ent, кроме ent/schema
|
||||
ent/*
|
||||
!ent/schema
|
||||
!ent/schema/**
|
||||
|
||||
# игнорируем маковский индекс
|
||||
.DS_Store
|
||||
|
||||
# игнорируем локальную конфигурацию
|
||||
# локальная конфигурация
|
||||
.env
|
||||
.vscode/
|
||||
.DS_Store
|
||||
|
||||
# игнорируем собранное приложение
|
||||
backend.run
|
||||
# собранные бинарники
|
||||
*.exe
|
||||
service
|
||||
proxy
|
||||
|
||||
# игнорируем отладочную информацию
|
||||
# отладочная информация
|
||||
src/__debug_*
|
||||
|
||||
9
Makefile
9
Makefile
@@ -1,5 +1,5 @@
|
||||
NAMESPACE ?= <PROJECT_CHANGE_ME>
|
||||
PACKAGE := backend
|
||||
NAMESPACE ?= proxy
|
||||
PACKAGE := ollama-proxy
|
||||
SHELL := /bin/bash
|
||||
REGISTRY := registry.halfakop.ru
|
||||
REPOSITORY := $(NAMESPACE)/$(PACKAGE)
|
||||
@@ -21,6 +21,7 @@ help:
|
||||
@printf " make test - Run unit tests\n"
|
||||
@printf " make test-integration - Run tests with integration tag\n"
|
||||
@printf " make run - Build then run locally\n"
|
||||
@printf " make dev - Run without building (go run, with env vars)\n"
|
||||
@printf " make clean - Clean build artifacts\n"
|
||||
@printf " make release - Clean, build image, login, push\n"
|
||||
@printf "\nVariables:\n"
|
||||
@@ -53,6 +54,10 @@ run: app
|
||||
@echo "Run application"
|
||||
@./${EXEC}
|
||||
|
||||
dev:
|
||||
@echo "Run in development mode"
|
||||
@OLLAMA_BACKEND=http://localhost:11434 LISTEN_ADDR=:8080 go run ./src
|
||||
|
||||
clean:
|
||||
@echo "Clean build environment"
|
||||
@rm -rf ./${EXEC}
|
||||
|
||||
94
README.md
94
README.md
@@ -1,42 +1,64 @@
|
||||
# НАЗВАНИЕ ПРОЕКТА — Бэкэнд — Go 1.24
|
||||
# Ollama Proxy
|
||||
|
||||
Этот репозиторий реализует сервис, который обеспечивает:
|
||||
* получение и сохранение метрик, полученных от внешней организации;
|
||||
* управление параметрами экспортёров метрик и планами экспорта.
|
||||
Прокси для подключения к серверу Ollama без SSH-туннелей.
|
||||
|
||||
## TL;DR
|
||||
---
|
||||
|
||||
Создание пользователя и базы для него:
|
||||
## Первый раз (один раз на ноутбуке)
|
||||
|
||||
createuser USERNAME -P <top_secret>
|
||||
createdb --owner USERNAME DBNAME
|
||||
### 1. Клонировать репозиторий
|
||||
|
||||
Проверка доступа:
|
||||
|
||||
psql -U USERNAME DBNAME
|
||||
|
||||
Для работы миграций надо сделать так:
|
||||
|
||||
psql -d DBNAME -c 'alter schema public owner to USERNAME;'
|
||||
|
||||
Затем:
|
||||
|
||||
```bash
|
||||
# 0) Подготовьте Postgres + переменные окружения
|
||||
export DATABASE_URL='postgres://USERNAME:top_secret@localhost:5432/DBNAME?sslmode=disable'
|
||||
|
||||
export PATH="$PATH:$(go env GOPATH)/bin"
|
||||
|
||||
# 1) Сгенерируйте код Ent по схемам (требуется один раз, при изменении схем)
|
||||
Подключите ваш ORM как сабмодуль Git.
|
||||
|
||||
go install entgo.io/ent/cmd/ent@latest
|
||||
ent generate ./orm/ent/schema
|
||||
|
||||
# 2) Примените миграции Atlas (готовые SQL в atlas/migrations)
|
||||
go install ariga.io/atlas/cmd/atlas@v0.38.0
|
||||
atlas migrate apply --dir file://orm/atlas/migrations --url "$DATABASE_URL"
|
||||
|
||||
# 3) Запустите сервис
|
||||
go run ./cmd/server
|
||||
```cmd
|
||||
git clone <репозиторий> C:\ollama-proxy (диск C: как пример)
|
||||
cd C:\ollama-proxy
|
||||
```
|
||||
|
||||
### 2. Собрать
|
||||
|
||||
```cmd
|
||||
go build -o proxy.exe ./src
|
||||
```
|
||||
|
||||
### 3. Создать файл `.env` рядом с `proxy.exe`
|
||||
|
||||
```
|
||||
LISTEN_ADDR=localhost:11434
|
||||
OLLAMA_BACKEND=http://10.111.111.40:8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Перед работой
|
||||
|
||||
**Терминал 1** — запустить прокси:
|
||||
|
||||
```cmd
|
||||
cd C:\ollama-proxy
|
||||
proxy.exe
|
||||
```
|
||||
|
||||
Дождись строки:
|
||||
|
||||
```
|
||||
Прокси запущен addr=localhost:11434 backend=http://10.111.111.40:8080
|
||||
```
|
||||
|
||||
**Терминал 2** — запустить Codex:
|
||||
|
||||
```cmd
|
||||
ollama launch codex
|
||||
```
|
||||
|
||||
Работаешь. Закончил — закрыл оба терминала.
|
||||
|
||||
---
|
||||
|
||||
## Проверка (если что-то не работает)
|
||||
|
||||
Убедись что прокси запущен и сервер доступен:
|
||||
|
||||
```cmd
|
||||
curl http://localhost:11434/api/tags
|
||||
```
|
||||
|
||||
Если вернулся список моделей — всё работает.
|
||||
|
||||
8
go.mod
8
go.mod
@@ -2,10 +2,4 @@ module backend
|
||||
|
||||
go 1.24.13
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/rs/cors v1.11.1
|
||||
)
|
||||
require github.com/joho/godotenv v1.5.1
|
||||
|
||||
8
go.sum
8
go.sum
@@ -1,10 +1,2 @@
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
|
||||
@@ -13,48 +13,40 @@ import (
|
||||
|
||||
// LoadConfig загружает конфиг из .env (если есть) и окружения.
|
||||
func LoadConfig(logger *slog.Logger) (*Config, error) {
|
||||
_ = godotenv.Load() // необязательно фейлиться, если файла нет
|
||||
_ = godotenv.Load()
|
||||
|
||||
cfg := &Config{}
|
||||
|
||||
cfg.Timezone = GetEnvAs("TIMEZONE", "UTC", ParseString)
|
||||
cfg.ServiceURL = GetEnvAs("SERVICE_URL", "http://localhost:8080", ParseString)
|
||||
cfg.ListenAddr = GetEnvAs("LISTEN_ADDR", ":8080", ParseString)
|
||||
cfg.BackendURL = GetEnvAs("OLLAMA_BACKEND", "http://localhost:11434", ParseString)
|
||||
|
||||
cfg.LoggingConfig.Instance = logger
|
||||
cfg.LoggingConfig.Level = GetEnvAs("LOG_LEVEL", "info", ParseString)
|
||||
cfg.LoggingConfig.ShowCfgDump = GetEnvAs("LOG_SHOW_DUMP", false, ParseBool)
|
||||
|
||||
cfg.DatabaseConfig = FillDatabaseConfig()
|
||||
|
||||
if cfg.LoggingConfig.ShowCfgDump {
|
||||
cfg.Print()
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// PrintConfig выводит конфигурацию (или любой другой struct) в виде таблички "KEY - VALUE".
|
||||
// Функция использует рефлексию для перебора полей структуры.
|
||||
// Print выводит конфигурацию в виде таблички "KEY - VALUE".
|
||||
func (c *Config) Print() {
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "Loaded configuration:")
|
||||
fmt.Fprintln(w, "KEY\tVALUE")
|
||||
fmt.Fprintln(w, "----\t-----")
|
||||
|
||||
// Получаем reflect.Value объекта.
|
||||
v := reflect.ValueOf(c)
|
||||
// Если передан указатель, получаем значение, на которое он указывает.
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
t := v.Type()
|
||||
|
||||
// Перебираем все поля структуры.
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
fieldName := t.Field(i).Name
|
||||
fieldValue := v.Field(i).Interface()
|
||||
|
||||
// Если поле имеет тип time.Duration, выводим его в виде строки.
|
||||
if d, ok := fieldValue.(time.Duration); ok {
|
||||
fieldValue = d.String()
|
||||
}
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DatabaseConfig struct {
|
||||
URL string
|
||||
Kind string
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Password string
|
||||
Name string
|
||||
UseTLS bool
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
func FillDatabaseConfig() DatabaseConfig {
|
||||
databaseURL := GetEnvAs("DATABASE_URL", "", ParseString)
|
||||
|
||||
if databaseURL == "" {
|
||||
return DatabaseConfig{}
|
||||
}
|
||||
|
||||
u, err := url.Parse(databaseURL)
|
||||
if err != nil {
|
||||
return DatabaseConfig{}
|
||||
}
|
||||
|
||||
kind := "postgres"
|
||||
if u.Scheme == "postgresql" {
|
||||
kind = "postgres"
|
||||
} else if u.Scheme != "" {
|
||||
kind = u.Scheme
|
||||
}
|
||||
|
||||
dbName := ""
|
||||
if len(u.Path) > 1 {
|
||||
dbName = u.Path[1:]
|
||||
}
|
||||
|
||||
return DatabaseConfig{
|
||||
URL: databaseURL,
|
||||
Kind: kind,
|
||||
Host: u.Hostname(),
|
||||
Port: u.Port(),
|
||||
User: u.User.Username(),
|
||||
Password: func() string {
|
||||
password, _ := u.User.Password()
|
||||
return password
|
||||
}(),
|
||||
Name: dbName,
|
||||
UseTLS: GetEnvAs("DATABASE_USETLS", false, ParseBool),
|
||||
Timeout: GetEnvAs("DATABASE_TIMEOUT", 30*time.Second, ParseDuration),
|
||||
}
|
||||
}
|
||||
|
||||
// GetDatabaseURLForLogging возвращает URL базы данных для логирования, скрывая пароль.
|
||||
func GetDatabaseURLForLogging(cfg *DatabaseConfig) (string, error) {
|
||||
if cfg.URL == "" {
|
||||
return "", fmt.Errorf("parameter DATABASE_URL is empty")
|
||||
}
|
||||
|
||||
u, err := url.Parse(cfg.URL)
|
||||
if err == nil {
|
||||
if u.User != nil {
|
||||
username := u.User.Username()
|
||||
s := u.Scheme + "://" + username + ":***@" + u.Host
|
||||
if u.Path != "" {
|
||||
s += u.Path
|
||||
}
|
||||
if u.RawQuery != "" {
|
||||
s += "?" + u.RawQuery
|
||||
}
|
||||
return s, err
|
||||
}
|
||||
}
|
||||
return u.String(), err
|
||||
}
|
||||
|
||||
func GetDatabaseDSN(cfg *DatabaseConfig) (string, error) {
|
||||
if cfg.URL == "" {
|
||||
return "", fmt.Errorf("parameter DATABASE_URL is empty")
|
||||
}
|
||||
|
||||
u, err := url.Parse(cfg.URL)
|
||||
if err == nil {
|
||||
query := u.Query()
|
||||
if !cfg.UseTLS {
|
||||
query.Set("sslmode", "disable")
|
||||
} else if query.Get("sslmode") == "" {
|
||||
query.Set("sslmode", "require")
|
||||
}
|
||||
u.RawQuery = query.Encode()
|
||||
return u.String(), err
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf(
|
||||
"host=%s port=%s user=%s password=%s dbname=%s",
|
||||
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.Name,
|
||||
)
|
||||
|
||||
if !cfg.UseTLS {
|
||||
dsn += " sslmode=disable"
|
||||
} else {
|
||||
dsn += " sslmode=require"
|
||||
}
|
||||
|
||||
return dsn, nil
|
||||
}
|
||||
@@ -9,9 +9,8 @@ type LoggingConfig struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Timezone string
|
||||
ServiceURL string
|
||||
ListenAddr string // env: LISTEN_ADDR, по умолчанию ":8080"
|
||||
BackendURL string // env: OLLAMA_BACKEND, по умолчанию "http://localhost:11434"
|
||||
|
||||
LoggingConfig
|
||||
DatabaseConfig
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"backend/src/internal/config"
|
||||
"backend/src/logic"
|
||||
"backend/src/models"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// глобальный хаб соединений WebSocket
|
||||
var hub sync.Map
|
||||
|
||||
// Настройки для обновления соединения до WebSocket
|
||||
var upgrader = websocket.Upgrader{
|
||||
// разрешаем соединения с любых источников
|
||||
// (для продакшена стоит ограничить)
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// WebSocketHubResponse отправляет ответ отдельному приложению
|
||||
func WebSocketHubResponse(packet models.ClientPacket) {
|
||||
cid := packet.ClientID
|
||||
msg := packet.Payload
|
||||
value, ok := hub.Load(cid)
|
||||
if ok {
|
||||
conn := value.(*websocket.Conn)
|
||||
log.Printf("[WS] Sending message: %s", msg)
|
||||
if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
|
||||
log.Printf("[WS] Send error: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[WS] Client <%s> unknown, skipping sending...", cid)
|
||||
}
|
||||
}
|
||||
|
||||
// Worker обрабатывает канал подписки и отправляет результат в WebSocket
|
||||
func Worker(ch models.ChannelOut) {
|
||||
log.Printf("[WS] Response worker is ready")
|
||||
for packet := range ch {
|
||||
WebSocketHubResponse(packet)
|
||||
}
|
||||
}
|
||||
|
||||
// Обработчик для WebSocket соединения
|
||||
func WebSocketHandler(ctx context.Context, cfg *config.Config,
|
||||
w http.ResponseWriter, r *http.Request, bl *logic.Business) {
|
||||
logger := cfg.LoggingConfig.Instance
|
||||
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
logger.Error("[WS] Unable to restart connection", "error", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// рассчитываем, что клиент присылает свой идентификатор, иначе создаём
|
||||
cid := r.URL.Query().Get("cid")
|
||||
if cid == "" {
|
||||
cid = uuid.New().String() // FIXME: или сбрасывать соединение
|
||||
}
|
||||
|
||||
// сохраняем соединение с клиентом в хабе
|
||||
hub.Store(cid, conn)
|
||||
logger.Info("[WS] Remote <%s> connected: %s", cid, conn.RemoteAddr())
|
||||
|
||||
// создаём канал для ответов в вебсокет
|
||||
wsResponseChannel := make(chan models.ClientPacket, 10000)
|
||||
|
||||
// Запускаем горутину, которая будет получать сообщения из канала и отправлять их клиенту.
|
||||
go Worker(wsResponseChannel)
|
||||
|
||||
// начинаем обработку сообщений от веб приложения
|
||||
for {
|
||||
messageType, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
logger.Info("[WS] Remote <%s> disconnected: %v:", cid, err)
|
||||
hub.Delete(cid)
|
||||
break
|
||||
}
|
||||
|
||||
// получаем общие поля
|
||||
var commonFields models.BaseFieldT
|
||||
if err := json.Unmarshal(message, &commonFields); err != nil {
|
||||
log.Println("[WS] Unable unmarshal common fields:", err)
|
||||
continue
|
||||
}
|
||||
// определяем тип сообщения
|
||||
switch commonFields.Action {
|
||||
case "COMMAND1":
|
||||
logger.Info("[WS] COMMAND1 requested!")
|
||||
/*
|
||||
var request models.CoordinatorConfigurationRequestT
|
||||
if err := json.Unmarshal(message, &request); err != nil {
|
||||
logger.Error("[WS] Unable unmarshal CoordinatorConfigurationRequestT")
|
||||
continue
|
||||
}
|
||||
|
||||
payload, err := bl.GetCoordinatorConfiguration(request.OrganizationID)
|
||||
|
||||
var packet models.CoordinatorConfigurationResponseT
|
||||
if err != nil {
|
||||
packet = models.CoordinatorConfigurationResponseT{
|
||||
BaseFieldT: commonFields,
|
||||
Status: 200,
|
||||
Payload: *payload,
|
||||
}
|
||||
} else {
|
||||
packet = models.CoordinatorConfigurationResponseT{
|
||||
BaseFieldT: commonFields,
|
||||
Status: 400,
|
||||
Payload: models.CoordinatorConfigurationT{},
|
||||
}
|
||||
}
|
||||
data, _ := json.Marshal(packet)
|
||||
wsResponseChannel <- models.ClientPacket{ClientID: cid, Payload: data}
|
||||
*/
|
||||
default:
|
||||
if messageType == websocket.TextMessage {
|
||||
log.Printf("[WS] Got text message: %s", string(message))
|
||||
} else {
|
||||
log.Printf("[WS] Got unknown message: %d", messageType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// ClientPacket используется для передачи пакетов через Go каналы
|
||||
type ClientPacket struct {
|
||||
ClientID string
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
type ChannelIn chan<- ClientPacket
|
||||
type ChannelOut <-chan ClientPacket
|
||||
|
||||
type WebSocketQueue struct {
|
||||
ctx context.Context
|
||||
ch chan ClientPacket
|
||||
}
|
||||
|
||||
func NewWebSocketQueue(ctx context.Context) *WebSocketQueue {
|
||||
q := WebSocketQueue{
|
||||
ctx: ctx,
|
||||
ch: make(chan ClientPacket, 10000),
|
||||
}
|
||||
|
||||
// Запускаем горутину, которая будет получать сообщения из канала
|
||||
// и отправлять их клиенту.
|
||||
go q.runWorker()
|
||||
return &q
|
||||
}
|
||||
|
||||
// runWorker обрабатывает канал подписк и отправляет результат в WebSocket
|
||||
func (q *WebSocketQueue) runWorker() {
|
||||
// создаём канал для ответов в вебсокет
|
||||
log.Printf("[WS] Response worker is ready")
|
||||
for packet := range q.ch {
|
||||
q.realSend(packet)
|
||||
}
|
||||
}
|
||||
|
||||
// Send на самом деле помещает пакет в очередь отправки, которая затем
|
||||
// разгребается с помощью WorkerOfQueuedResponsesToWS.
|
||||
func (q *WebSocketQueue) Send(packet ClientPacket) {
|
||||
q.ch <- packet
|
||||
}
|
||||
|
||||
// WebSocketHubResponse отправляет ответ отдельному приложению
|
||||
func (q *WebSocketQueue) realSend(packet ClientPacket) {
|
||||
cid := packet.ClientID
|
||||
msg := packet.Payload
|
||||
value, ok := hub.Load(cid)
|
||||
if ok {
|
||||
conn := value.(*websocket.Conn)
|
||||
log.Printf("[WS] Sending message: %s", msg)
|
||||
if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
|
||||
log.Printf("[WS] Send error: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[WS] Client <%s> unknown, skipping sending...", cid)
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,11 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// New creates a slog logger with the provided level string (e.g., "debug", "info").
|
||||
func New(level string) *slog.Logger {
|
||||
return slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: ParseLevel(level)}))
|
||||
}
|
||||
|
||||
// ParseLevel converts a string level to slog.Level, defaults to info on unknown.
|
||||
|
||||
func ParseLevel(lvl string) slog.Level {
|
||||
switch strings.ToLower(lvl) {
|
||||
case "debug":
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"backend/ent"
|
||||
"context"
|
||||
)
|
||||
|
||||
type Business struct {
|
||||
ctx context.Context
|
||||
db *ent.Client
|
||||
}
|
||||
|
||||
func NewBusinessLogic(ctx context.Context, client *ent.Client) *Business {
|
||||
return &Business{
|
||||
ctx: ctx,
|
||||
db: client,
|
||||
}
|
||||
}
|
||||
116
src/main.go
116
src/main.go
@@ -1,110 +1,86 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"backend/ent"
|
||||
"backend/src/internal/config"
|
||||
"backend/src/internal/gateway"
|
||||
"backend/src/internal/logging"
|
||||
"backend/src/logic"
|
||||
|
||||
_ "github.com/lib/pq" // побочный импорт драйвера PostgreSQL
|
||||
"github.com/rs/cors"
|
||||
)
|
||||
|
||||
const (
|
||||
AppName = "Backend"
|
||||
AppName = "Ollama Proxy"
|
||||
AppVersion = "1.0.0"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
logger := logging.New("info")
|
||||
slog.SetDefault(logger)
|
||||
|
||||
logger.Info(fmt.Sprintf("Starting %s version %s\n", AppName, AppVersion))
|
||||
logger.Info(fmt.Sprintf("Starting %s version %s", AppName, AppVersion))
|
||||
|
||||
cfg, err := config.LoadConfig(logger)
|
||||
if err != nil {
|
||||
logger.Error("Configuration loading error", "error", err)
|
||||
logger.Error("Ошибка конфигурации", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// adjust logger according to LOG_LEVEL
|
||||
level := logging.ParseLevel(cfg.LoggingConfig.Level)
|
||||
if level != slog.LevelInfo {
|
||||
logger.Info("Adjusting log level from env", "level", level.String())
|
||||
logger.Info("Уровень логирования из env", "level", level.String())
|
||||
}
|
||||
logger = logging.New(cfg.LoggingConfig.Level)
|
||||
slog.SetDefault(logger)
|
||||
cfg.LoggingConfig.Instance = logger
|
||||
|
||||
// создаем контекст с отменой для управления жизненным циклом сервиса.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// подключаемся к базе данных
|
||||
dbURL, err := config.GetDatabaseURLForLogging(&cfg.DatabaseConfig)
|
||||
target, err := url.Parse(cfg.BackendURL)
|
||||
if err != nil {
|
||||
logger.Error("Failed getting database URL for logging", "error", err)
|
||||
return
|
||||
}
|
||||
logger.Info("Connecting to database...", "url", dbURL)
|
||||
|
||||
dsn, err := config.GetDatabaseDSN(&cfg.DatabaseConfig)
|
||||
if err != nil {
|
||||
logger.Error("Failed getting database DSN", "error", err)
|
||||
return
|
||||
}
|
||||
db, err := ent.Open(cfg.DatabaseConfig.Kind, dsn)
|
||||
if err != nil {
|
||||
logger.Error("Failed opening connection to postgres", "error", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Применяем миграции
|
||||
if err := db.Schema.Create(ctx); err != nil {
|
||||
logger.Error("Failed creating schema resources", "error", err)
|
||||
logger.Error("Неверный URL бэкенда", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// инициализируем бизнес логику
|
||||
business := logic.NewBusinessLogic(ctx, db)
|
||||
|
||||
// регистрируем обработчик WebSocket по адресу /ws
|
||||
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
gateway.WebSocketHandler(ctx, cfg, w, r, business)
|
||||
})
|
||||
|
||||
// создаём CORS middleware
|
||||
corsMiddleware := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"http://localhost:4200"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Content-Type", "Authorization"},
|
||||
AllowCredentials: true,
|
||||
})
|
||||
|
||||
// оборачиваем стандартный mux (DefaultServeMux)
|
||||
handler := corsMiddleware.Handler(http.DefaultServeMux)
|
||||
|
||||
// адрес сервера должен быть без схемы
|
||||
clearURL := cfg.ServiceURL
|
||||
if strings.Contains(clearURL, "://") {
|
||||
if u, err := url.Parse(clearURL); err == nil && u.Host != "" {
|
||||
clearURL = u.Host
|
||||
}
|
||||
proxy := &httputil.ReverseProxy{
|
||||
Rewrite: func(r *httputil.ProxyRequest) {
|
||||
r.SetURL(target)
|
||||
r.Out.Host = target.Host
|
||||
},
|
||||
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
logger.Error("Ошибка прокси", "method", r.Method, "path", r.URL.Path, "error", err)
|
||||
http.Error(w, "Bad Gateway", http.StatusBadGateway)
|
||||
},
|
||||
}
|
||||
|
||||
logger.Info("WebSocket server is on", cfg.ServiceURL)
|
||||
if err := http.ListenAndServe(clearURL, handler); err != nil {
|
||||
logger.Error("WebSocket server is unable to start", "error", err)
|
||||
return
|
||||
handler := loggingMiddleware(logger, proxy)
|
||||
|
||||
logger.Info("Прокси запущен", "addr", cfg.ListenAddr, "backend", cfg.BackendURL)
|
||||
if err := http.ListenAndServe(cfg.ListenAddr, handler); err != nil {
|
||||
logger.Error("Ошибка сервера", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func loggingMiddleware(logger *slog.Logger, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
rw := &responseWriter{ResponseWriter: w, status: http.StatusOK}
|
||||
next.ServeHTTP(rw, r)
|
||||
logger.Info("запрос",
|
||||
"method", r.Method,
|
||||
"path", r.URL.Path,
|
||||
"status", rw.status,
|
||||
"duration", time.Since(start).String(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(code int) {
|
||||
rw.status = code
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package models
|
||||
|
||||
type BaseFieldT struct {
|
||||
TaskID int `json:"taskId" binding:"required"`
|
||||
Kind string `json:"kind" binding:"required"`
|
||||
Action string `json:"action" binding:"required"`
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package models
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
// ClientPacket используется для передачи пакетов через Go каналы
|
||||
type ClientPacket struct {
|
||||
ClientID string
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
// ClientRequest используется для передачи запросов от клиента
|
||||
type ClientRequest struct {
|
||||
TaskID uuid.UUID `json:"taskId" binding:"required"`
|
||||
Kind string `json:"kind" binding:"required"`
|
||||
Action string `json:"action" binding:"required"`
|
||||
}
|
||||
|
||||
// ClientResponse используется для передачи ответов клиенту
|
||||
type ClientResponse struct {
|
||||
ClientRequest
|
||||
Status int `json:"status" binding:"required"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Payload any `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
type ChannelIn chan<- ClientPacket
|
||||
type ChannelOut <-chan ClientPacket
|
||||
Reference in New Issue
Block a user