1
0
mirror of https://github.com/vsilchenkov/catcher.git synced 2025-10-30 23:43:51 +02:00
This commit is contained in:
Владимир Сильченков
2025-07-13 14:25:24 +03:00
commit 77170424be
65 changed files with 8009 additions and 0 deletions

0
.env Normal file
View File

22
.gitignore vendored Normal file
View File

@@ -0,0 +1,22 @@
*.exe
*.exe~
*.dll
*.so
*.dylib
*.log
.idea
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
out.*
profile
*.syso
# Dependency directories (remove the comment below to include it)
# vendor/
# .env
web

28
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,28 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Debug",
"type": "go",
"request": "launch",
"mode": "auto",
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}\\app\\cmd\\catcher\\main.go",
"args": ["-config=config/config_debug.yml"],
"envFile": "${workspaceFolder}\\.env"
},
{
"name": "Production",
"type": "go",
"request": "launch",
"mode": "auto",
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}\\app\\cmd\\catcher\\main.go",
"args": [],
"envFile": "${workspaceFolder}\\.env"
}
]
}

7
Makefile Normal file
View File

@@ -0,0 +1,7 @@
.PHONY: build-win
build-win:
go test -v ./...
cd app/cmd/catcher && goversioninfo versioninfo.json
go build -o catcher.exe ./app/cmd/catcher
rm -f app/cmd/catcher/resource.syso

12
README.md Normal file
View File

@@ -0,0 +1,12 @@
# Catcher
## Сервис регистрации ошибок и отправкой их в Sentry
## Build
- `make build-win`
- `go build .\app\cmd\catcher`
## Swagger
- `​swag init -g app/cmd/catcher/main.go​ --parseDependency`

53
app/build/build.go Normal file
View File

@@ -0,0 +1,53 @@
package build
import (
"os"
"path/filepath"
"github.com/kardianos/service"
)
const ProjectName = "Cather"
var Version = "1.5.0"
var Time string
var User string
type Option struct {
Version string
ProjectName string
WorkingDir string
Interactive bool
}
func NewOption() *Option {
return &Option{
ProjectName: ProjectName,
Version: Version,
Interactive: service.Interactive(),
WorkingDir: WorkingDir(),
}
}
// Получить абсолютный путь к папке, где лежит .exe или запущена программа
func WorkingDir() string {
if service.Interactive() {
cwd, err := os.Getwd()
if err != nil {
return "."
}
return cwd
} else {
exePath, err := os.Executable()
if err != nil {
return "."
}
return filepath.Dir(exePath)
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

BIN
app/cmd/catcher/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

95
app/cmd/catcher/main.go Normal file
View File

@@ -0,0 +1,95 @@
package main
import (
"catcher/app/build"
"catcher/app/internal/config"
"catcher/app/internal/lib/caching"
"catcher/app/internal/lib/caching/memory"
"catcher/app/internal/lib/logging"
"catcher/app/internal/models"
"catcher/app/internal/server"
"catcher/app/internal/server/http"
"context"
"fmt"
"log"
"os"
"time"
"github.com/getsentry/sentry-go"
"github.com/kardianos/service"
"github.com/cockroachdb/errors"
)
// @title Catcher
// @version 1.0
// @description Catcher API Service
// @host localhost:8000
// @BasePath /api
func main() {
ctx := context.Background()
name := build.ProjectName
svcConfig := &service.Config{
Name: name + "Service",
DisplayName: name + " API Service",
Description: "API-приложение " + name,
}
flags := config.ParseFlags()
c, err := config.LoadSettigs(flags)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if c.Sentry.Use {
err = sentry.Init(logging.SentryClientOptions(c))
if err != nil {
log.Fatal(err)
}
defer sentry.Flush(2 * time.Second)
}
logger := logging.Initlogger(c)
defer func() {
if r := recover(); r != nil {
var err error
switch e := r.(type) {
case error:
err = e
case string:
err = errors.New(e)
default:
err = fmt.Errorf("panic: %v", e)
}
err = errors.WithStackDepth(err,2)
logger.Error("Panic recovered",
logger.Err(err),
)
os.Exit(1)
}
}()
cacher := memory.New()
svcCacher := caching.New(cacher)
appCtx := models.NewAppContext(ctx, c, svcCacher, logger)
srv := http.New(appCtx)
prg := server.NewProgram(srv, appCtx)
s, err := service.New(prg, svcConfig)
if err != nil {
logger.Error("Error on service start",
logger.Err(err))
}
err = s.Run()
if err != nil {
logger.Error("Ошибка запуска сервера",
logger.Err(err))
}
}

View File

@@ -0,0 +1,40 @@
{
"FixedFileInfo": {
"FileVersion": {
"Major": 1,
"Minor": 5,
"Patch": 0,
"Build": 0
},
"FileFlagsMask": "3f",
"FileFlags": "00",
"FileOS": "040004",
"FileType": "01",
"FileSubType": "00"
},
"StringFileInfo": {
"ProductVersion": "1.5.0",
"CompanyName": "Vozovoz",
"FileDescription": "API Service",
"InternalName": "catcher",
"LegalCopyright": "© 2025 Vozovoz",
"LegalTrademarks": "",
"OriginalFilename": "catcher.exe",
"PrivateBuild": "",
"ProductName": "Catcher API Service",
"SpecialBuild": "",
"Comments": ""
},
"VarFileInfo": {
"Translation": {
"LangID": 1049,
"CharsetID": 1200
}
},
"IconPath": "icon.ico",
"Manifest": {
"ExecutionLevel": "asInvoker",
"UIAccess": false,
"DPI_Awareness": "PerMonitorV2"
}
}

View File

@@ -0,0 +1,168 @@
package config
import (
"flag"
"fmt"
"os"
"path/filepath"
"catcher/app/build"
"github.com/creasty/defaults"
"github.com/go-playground/validator/v10"
"github.com/joho/godotenv"
"github.com/cockroachdb/errors"
"gopkg.in/yaml.v3"
)
const (
logLevel_debug int = 5
sentry_timeout int = 5
)
type Config struct {
build.Option
Server struct {
Port string `yaml:"Port" binding:"required"`
} `yaml:"Server" binding:"required"`
Registry struct {
UserMessage string `yaml:"UserMessage" binding:"required"`
DumpType int `yaml:"DumpType" binding:"required"`
Timeout int `yaml:"Timeout" binding:"required"`
} `yaml:"Registry"`
Projects []Project `yaml:"Projects" validate:"required,dive"`
Log struct {
Debug bool `yaml:"Debug" binding:"required"`
Level int `yaml:"Level" binding:"required"`
Dir string `yaml:"Dir" binding:"required"`
OutputInFile bool `yaml:"OutputInFile" binding:"required"`
} `yaml:"Log" binding:"required"`
DeleteTempFiles bool `yaml:"DeleteTempFiles" binding:"required"`
Sentry struct {
Use bool `yaml:"Use"`
Dsn string `yaml:"Dsn"`
AttachStacktrace bool `yaml:"AttachStacktrace"`
TracesSampleRate float64 `yaml:"TracesSampleRate"`
EnableTracing bool `yaml:"EnableTracing"`
} `yaml:"Sentry"`
}
type flags struct {
configPath string
}
func newConfig(b build.Option) *Config {
return &Config{
Option: b,
}
}
func ParseFlags() flags {
var debug bool
var configPath string
flag.BoolVar(&debug, "debug", false, "Use debug")
flag.StringVar(&configPath, "config", "config/config.yml", "Путь к файлу настроек")
flag.Parse()
flags := flags{
configPath: configPath,
}
return flags
}
func LoadSettigs(flags flags) (*Config, error) {
const op = "config.LoadSettigs"
b := build.NewOption()
path := flags.configPath
fullPath := filepath.Join(b.WorkingDir, path)
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
return nil, errors.WithMessagef(err, "%s - файл настроек %q не найден", op, fullPath)
}
file, err := os.ReadFile(fullPath)
if err != nil {
return nil, errors.WithMessagef(err, "%s - ошибка чтения файла %q", op, fullPath)
}
c := newConfig(*b)
if err := yaml.Unmarshal(file, &c); err != nil {
return nil, errors.WithMessagef(err, "%s - ошибка десириализации настроек", op)
}
validate := validator.New()
err = validate.Struct(c)
if err != nil {
return nil, errors.WithMessagef(err, "%s - ошибка валидации настроек", op)
}
if err := defaults.Set(c); err != nil {
return nil, errors.WithMessagef(err, "%s - ошибка установки настроек по умолчанию", op)
}
if c.Registry.Timeout == 0 {
c.Registry.Timeout = sentry_timeout
}
loadEnv(c)
fmt.Printf("Settings loaded: %s\n", fullPath)
return c, nil
}
func loadEnv(c *Config) {
godotenv.Load()
Dsn := os.Getenv("SENTRY_DSN")
if Dsn != "" {
c.Sentry.Dsn = Dsn
}
}
func (c Config) UseDebug() bool {
return c.Log.Debug
}
func (c Config) ServerPort() string {
return c.Server.Port
}
func (c Config) RegistryUserMessage() string {
return c.Registry.UserMessage
}
func (c Config) RegistryDumpType() int {
return c.Registry.DumpType
}
func (c Config) ProjectByName(name string) (Project, error) {
for _, v := range c.Projects {
if v.Name == name {
return v, nil
}
}
return Project{}, errors.New(fmt.Sprintf("Настройки проекта не найдены по имени: %s", name))
}
func (c Config) ProjectById(id string) (Project, error) {
for _, v := range c.Projects {
if v.Id == id {
return v, nil
}
}
return Project{}, errors.New(fmt.Sprintf("Настройки проекта не найдены по ID: %s", id))
}

View File

@@ -0,0 +1,91 @@
package config
import (
"catcher/app/internal/git/gitlab"
"slices"
"github.com/jinzhu/copier"
)
type Project struct {
Name string `yaml:"Name" binding:"required"`
Id string `yaml:"ID" binding:"required"`
Service struct {
Use bool `yaml:"Use"`
Url string `yaml:"Url"`
IimeOut int `yaml:"IimeOut"`
Credintials struct {
UserName string `yaml:"UserName"`
Password string `yaml:"Password"`
} `yaml:"Credintials"`
Cache cache `yaml:"Cache"`
Exeptions struct {
Use bool `yaml:"Use"`
Cache cache `yaml:"Cache"`
} `yaml:"Exeptions"`
Test struct {
UserName string `yaml:"UserName"`
} `yaml:"Test"`
} `yaml:"Service"`
release string `yaml:"Release"`
Sentry struct {
Dsn string `yaml:"Dsn" binding:"required"`
Environment string `yaml:"Environment" binding:"required"`
Platform string `yaml:"Platform" binding:"required"`
ContextAround struct {
Use bool `yaml:"Use"`
Quantity int `yaml:"Quantity"`
Cache cache `yaml:"Cache"`
} `yaml:"ContextAround"`
Attachments struct {
Use bool `yaml:"Use" binding:"required"`
Сompress struct {
Use bool `yaml:"Use"`
Percent int `yaml:"Percent" validate:"required,min=1,max=100"`
} `yaml:"Сompress"`
} `yaml:"Attachments"`
SendingCache cache `yaml:"SendingCache"`
} `yaml:"Sentry" binding:"required"`
Git struct {
Use bool `yaml:"Use"`
Url string `yaml:"Url"`
Path string `yaml:"Path"`
Token string `yaml:"Token"`
Branch string `yaml:"Branch"`
SourceCodeRoot string `yaml:"SourceCodeRoot"`
} `yaml:"Git"`
Extentions []string `yaml:"Extentions"`
}
type cache struct {
Use bool `yaml:"Use"`
Expiration int `yaml:"Expiration" validate:"required,min=1"`
}
func (p *Project) SetRelease(release string) {
p.release = release
}
func (p Project) Release() string {
return p.release
}
func (p Project) ExistExtention(input string) bool {
return slices.Contains(p.Extentions, input)
}
type gitter interface {
GetFileContent(filePath string) (*string, error)
}
func (p Project) GetGit() (gitter, error) {
if !p.Git.Use {
return nil, nil
}
var prjGit gitlab.Project
copier.Copy(&prjGit, &p.Git)
return gitlab.Get(prjGit)
}

121
app/internal/git/git.go Normal file
View File

@@ -0,0 +1,121 @@
package git
import (
"catcher/app/internal/config"
"catcher/app/internal/models"
"fmt"
"strings"
"time"
)
type Gitter interface {
GetFileContent(filePath string) (*string, error)
}
type ContextAround struct {
Pre []string
Post []string
}
type Service struct {
Gitter
models.AppContext
prj config.Project
}
func New(g Gitter, prj config.Project, appCtx models.AppContext) *Service {
if g == nil || !prj.Sentry.ContextAround.Use {
return nil
}
return &Service{
Gitter: g,
AppContext: appCtx,
prj: prj,
}
}
func (s Service) GetContextAround(filePath string, lineno int) ContextAround {
const op = "exceptionbsl.prePostContext"
prj := s.prj
if filePath == "" {
return ContextAround{}
}
useCache := prj.Sentry.ContextAround.Cache.Use
key := fmt.Sprintf("%s:%s:%s:%d", op, prj.Name, filePath, lineno)
if useCache {
if x, found := s.Cacher.Get(s.Ctx, key); found {
s.Logger.Debug("Используем кэш ContextAround",
s.Logger.Op(op),
s.Logger.Str("key", key))
return x.(ContextAround)
}
}
content, err := s.Gitter.GetFileContent(filePath)
if err != nil {
s.Logger.Warn("Не удалось получить контент из git",
s.Logger.Err(err),
s.Logger.Str("filePath", filePath),
s.Logger.Op(op))
return ContextAround{}
}
lines := strings.Split(*content, "\n")
lenLines := len(lines)
if lineno > lenLines {
return ContextAround{}
}
quantity := prj.Sentry.ContextAround.Quantity
indexLineno := lineno - 1
var pre, post []string
start := indexLineno - quantity
start = max(start, 0)
start = min(start, lenLines-1)
qSart := indexLineno - start
if qSart > 0 {
pre = make([]string, qSart)
count := 0
for i := start; i <= indexLineno-1; i++ {
pre[count] = lines[i]
count++
}
}
end := indexLineno + quantity
end = min(end, lenLines-1)
qEnd := end - indexLineno
if qEnd > 0 {
post = make([]string, qEnd)
count := 0
for i := indexLineno + 1; i <= end; i++ {
post[count] = lines[i]
count++
}
}
res := ContextAround{
Pre: pre,
Post: post,
}
s.Logger.Debug("Получен контент из git",
s.Logger.Op(op),
s.Logger.Str("filePath", filePath))
if useCache {
s.Cacher.Set(s.Ctx, key, res, time.Duration(prj.Sentry.ContextAround.Cache.Expiration)*time.Minute)
}
return res
}

View File

@@ -0,0 +1,75 @@
package gitlab
import (
"sync"
"github.com/cockroachdb/errors"
gitlab "gitlab.com/gitlab-org/api/client-go"
)
type Git struct {
*gitlab.Client
Project Project
}
type Project struct {
Url string
Path string
Token string
Branch string
}
type gitssmap map[Project]Git
var gits gitssmap
var gitsMu sync.RWMutex
func init() {
gits = make(gitssmap)
}
func New(prj Project) (*Git, error) {
g, err := gitlab.NewClient(prj.Token, gitlab.WithBaseURL(prj.Url))
if err != nil {
return nil, errors.Wrap(err, "Failed to create client")
}
git := Git{g, prj}
gitsMu.Lock()
gits[prj] = git
gitsMu.Unlock()
return &git, nil
}
func Get(prj Project) (*Git, error) {
gitsMu.RLock()
git, ok := gits[prj]
gitsMu.RUnlock()
if ok {
return &git, nil
}
return New(prj)
}
func (g Git) GetFileContent(filePath string) (*string, error) {
gf := &gitlab.GetRawFileOptions{
Ref: gitlab.Ptr(g.Project.Branch),
}
file, _, err := g.RepositoryFiles.GetRawFile(g.Project.Path, filePath, gf)
if err != nil {
return nil, errors.Wrap(err, "Failed to get content")
}
content := string(file)
return &content, nil
}

View File

@@ -0,0 +1,113 @@
package handler
import (
"catcher/app/internal/config"
"catcher/app/internal/lib/logging"
"catcher/app/internal/models"
"catcher/app/internal/service"
_ "catcher/docs"
"os"
"github.com/getsentry/sentry-go"
sentrygin "github.com/getsentry/sentry-go/gin"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
type Handler struct {
services *service.Services
config *config.Config
logger logging.Logger
}
func NewHandler(service *service.Services, appCtx models.AppContext) *Handler {
return &Handler{
services: service,
config: appCtx.Config,
logger: appCtx.Logger}
}
func (h *Handler) InitHandler() *gin.Engine {
debug := h.config.UseDebug()
if !debug {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
router.Use(gin.Recovery())
if debug {
router.Use(h.initCustumLogger())
} else {
if h.config.Interactive {
router.Use(gin.Logger())
}
}
if h.config.Sentry.Use {
router.Use(h.initSentryGin())
}
router.GET("/", h.swagger)
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
api := router.Group("/api")
api.GET("/", h.swagger)
{
registry := api.Group("/reg")
{
registry.GET("/", h.getInfo)
registry.POST("/getInfo", h.getInfoPost)
registry.POST("/pushReport", h.pushReport)
}
prj := api.Group("/prj/:id")
{
prj.POST("/sendEvent", h.sendEvent)
}
service := api.Group("/service")
{
service.GET("/clearCache", h.clearCache)
}
}
return router
}
func (h *Handler) initCustumLogger() gin.HandlerFunc {
var out *os.File
out = os.Stdout
settngs := h.config
if settngs.Log.OutputInFile {
fileName := "api.log"
file, err := logging.GetOutputLogFile(settngs.WorkingDir, settngs.Log.Dir, fileName)
if err == nil {
out = file
} else {
h.logger.Error("Не удалось открыть файл логов, используется стандартный stderr",
h.logger.Str("name", fileName),
h.logger.Err(err))
}
}
custumlogger := gin.LoggerWithWriter(out)
return custumlogger
}
func (h *Handler) initSentryGin() gin.HandlerFunc {
if err := sentry.Init(logging.SentryClientOptions(h.config)); err != nil {
h.logger.Error("Sentry initialization failed",
h.logger.Err(err))
}
return sentrygin.New(sentrygin.Options{
Repanic: true,
})
}

View File

@@ -0,0 +1,40 @@
package handler
import (
"catcher/app/internal/models"
"net/http"
"github.com/gin-gonic/gin"
)
// @Summary Send Event
// @Tags Event
// @Description Отправка Event в Sentry
// @ID sendEvent
// @Accept json
// @Produce json
// @Param input body models.Event true "Данные события"
// @Success 200 {object} models.SendEventResult
// @Failure default {object} errorResponse
// @Router /api/prj/:id/sendEvent [post]
func (h *Handler) sendEvent(c *gin.Context) {
const op = "projecty.sendEvent"
var input models.Event
if err := c.ShouldBindJSON(&input); err != nil {
newErrorResponse(c, http.StatusBadRequest, op+".ShouldBindJSON", err)
return
}
projectId := c.Param("id")
eventId, err := h.services.Projecty.SendEvent(projectId, input)
if err != nil {
newErrorResponse(c, http.StatusInternalServerError, op, err)
return
}
c.JSON(http.StatusOK, gin.H{"event_id": eventId})
}

View File

@@ -0,0 +1,90 @@
package handler
import (
"io"
"net/http"
"catcher/app/internal/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// @Summary Get Info
// @Tags info
// @Description Проверка работы метода getInfo
// @ID getInfo
// @Produce json
// @Success 200 {object} models.RegistryInfo
// @Failure default {object} errorResponse
// @Router /api/reg [get]
func (h *Handler) getInfo(c *gin.Context) {
input := models.NewRegistryInput()
info := h.services.Registry.GetInfo(input)
c.JSON(http.StatusOK, info)
}
// @Summary Get Info Post
// @Tags Info
// @Description Получение информации для отчета об ошибки
// @ID getInfoPost
// @Accept json
// @Produce json
// @Param input body models.RegistryInput true "Значения для отчета об ошибке"
// @Success 200 {object} models.RegistryInfo
// @Failure default {object} errorResponse
// @Router /api/reg/getInfo [post]
func (h *Handler) getInfoPost(c *gin.Context) {
const op = "registry.getInfoPost"
var input models.RegistryInput
if err := c.ShouldBindJSON(&input); err != nil {
newErrorResponse(c, http.StatusBadRequest, op, err)
return
}
info := h.services.Registry.GetInfo(input)
c.JSON(http.StatusOK, info)
}
// @Summary Push Report
// @Tags Report
// @Description Отправка отчета об ошибки
// @ID pushReport
// @Accept multipart/form-data
// @Produce json
// @Param file formData file true "Файл в архиве формата https://its.1c.ru/db/v8327doc#bookmark:dev:TI000002558"
// @Success 200 {object} models.RegistryPushReportResult
// @Failure default {object} errorResponse
// @Router /api/reg/pushReport [post]
func (h *Handler) pushReport(c *gin.Context) {
const op = "registry.pushReport"
data, err := io.ReadAll(c.Request.Body)
if err != nil {
newErrorResponse(c, http.StatusBadRequest, op+".io.ReadAll", err)
return
}
id := uuid.New().String()
input := models.RegistryPushReportInput{
ID: id,
Data: data,
}
result, err := h.services.Registry.PushReport(input)
if err != nil {
newErrorResponse(c, http.StatusInternalServerError, op, err)
return
}
c.JSON(http.StatusOK, result)
}

View File

@@ -0,0 +1,20 @@
package handler
import (
"catcher/app/internal/lib/logging"
"github.com/gin-gonic/gin"
)
type errorResponse struct {
Message string `json:"message"`
}
func newErrorResponse(c *gin.Context, statusCode int, op string, err error) {
logger := logging.GetLogger()
logger.Error("Error response",
logger.Str("Request", c.Request.RequestURI),
logger.Op(op),
logger.Err(err))
c.AbortWithStatusJSON(statusCode, errorResponse{err.Error()})
}

View File

@@ -0,0 +1,35 @@
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
)
func (h *Handler) swagger(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/swagger/index.html")
}
// @Summary Clear cache
// @Tags Cache
// @Description Очищает все данные, сохранённые в кэше
// @ID clearCache
// @Produce json
// @Success 200 {object} map[string]string
// @Failure default {object} errorResponse
// @Router /api/service/clearCache [get]
func (h *Handler) clearCache(c *gin.Context) {
const op = "handler.service.clearCache"
if err := h.services.Service.ClearCache(); err != nil {
newErrorResponse(c, http.StatusBadRequest, op, err)
return
}
c.JSON(http.StatusOK, gin.H{
"result": "ok",
})
}

View File

@@ -0,0 +1,22 @@
package caching
import (
"context"
"time"
)
type Cacher interface {
Get(ctx context.Context, key string) (any, bool)
Set(ctx context.Context, key string, x any, d time.Duration)
Clear(ctx context.Context) error
}
type Service struct {
Cacher
}
func New(c Cacher) Service {
return Service{
Cacher: c,
}
}

View File

@@ -0,0 +1,30 @@
package memory
import (
"context"
"time"
"github.com/patrickmn/go-cache"
)
type Cacher struct {
*cache.Cache
}
func New() Cacher {
cacher := cache.New(5*time.Minute, 10*time.Minute)
return Cacher{cacher}
}
func (c Cacher) Get(_ context.Context, key string) (any, bool) {
return c.Cache.Get(key)
}
func (c Cacher) Set(_ context.Context, key string, x any, d time.Duration) {
c.Cache.Set(key, x, d)
}
func (c Cacher) Clear(_ context.Context) error {
c.Cache.Flush()
return nil
}

View File

@@ -0,0 +1,263 @@
package gitbsl
import (
"catcher/app/internal/lib/logging"
"errors"
"strings"
)
const (
ExtProcessing = "ВнешняяОбработка"
ExtReport = "ВнешнийОтчет"
Expansion = "bsl"
PathSeparator = "/"
)
type Config struct {
SourceCodeRoot string
}
type Path struct {
Config
Value string
Logger logging.Logger
}
func IsExternalModule(m string) bool {
return strings.HasPrefix(m, ExtProcessing) || strings.HasPrefix(m, ExtReport)
}
func IsExpansion(m string) bool {
return strings.Contains(m, " ") // имя расширения и путь к объекту разделяются пробелом
}
func NewConfig(sourceCodeRoot string) Config {
return Config{
SourceCodeRoot: sourceCodeRoot,
}
}
func NewPath(value string, sourceCodeRoot string, logger logging.Logger) Path {
return Path{
Config: NewConfig(sourceCodeRoot),
Value: strings.TrimSpace(value),
Logger: logger}
}
func (p Path) AbsPath() (string, error) {
path := strings.TrimSpace(p.Value)
paths := make([]string, 0)
modules := make([]string, 0)
parts := strings.Split(path, ".")
lenParts := len(parts)
position := 0
for _, segment := range parts {
position++
switch position {
case 1: //Базовый тип
base := p.translateBaseType(segment, true)
if base == "" {
return "", errors.New("базовый путь не определен")
}
paths = append(paths, base)
modules = p.addModulesPath(modules, segment, false)
case 2: //Имя объекта метаданных
paths = append(paths, segment)
modules = p.addModulesPath(modules, segment, false)
case 3: //Форма, команда, модуль объекта, модуль менеджера,
if lenParts > 3 {
// Добавляем только, если есть еще часть
base := p.translateObjectName(segment, false)
if base != "" {
paths = append(paths, base)
}
}
modules = p.addModulesPath(modules, segment, false)
case 4: //Имя формы, имя команды
paths = append(paths, segment)
modules = p.addModulesPath(modules, segment, false)
}
}
paths = append(paths, modules...)
if len(paths) == 0 {
return "", nil
}
result := p.SourceCodeRoot +
PathSeparator +
strings.Join(paths, PathSeparator) +
"." +
Expansion
p.Logger.Debug("AbsPath",
p.Logger.Str("input", p.Value),
p.Logger.Str("output", result))
return result, nil
}
func (p Path) translateBaseType(base string, warn bool) string {
const op = "gitbsl.translateBaseType"
result, ok := mapBasesTypes()[base]
if !ok {
if warn {
p.Logger.Warn("Неизвестное имя типа",
p.Logger.Op(op),
p.Logger.Str("name", base),
p.Logger.Str("value", p.Value),
p.Logger.Err(errors.New("тип не найден в mapBasesTypes")))
}
return ""
}
return result
}
func (p Path) translateObjectName(base string, warn bool) string {
const op = "gitbsl.translateObjectName"
result, ok := mapObjectNames()[base]
if !ok {
if warn {
p.Logger.Warn("Неизвестное имя объекта",
p.Logger.Op(op),
p.Logger.Str("name", base),
p.Logger.Str("value", p.Value),
p.Logger.Err(errors.New("тип не найден в mapObjectNames")))
}
return ""
}
return result
}
func (p Path) addModulesPath(pathModules []string, name string, warn bool) []string {
const op = "gitbsl.addPartPathModulesGit"
if name == "" {
return pathModules
}
modules, ok := mapModulesPath()[name]
if !ok {
if warn {
p.Logger.Warn("Неизвестное имя модуля",
p.Logger.Op(op),
p.Logger.Str("name", name),
p.Logger.Str("value", p.Value),
p.Logger.Err(errors.New("имя модуля не найдено в mapModulesPath")))
}
return pathModules
}
pathModules = append(pathModules, modules...)
return pathModules
}
func mapBasesTypes() map[string]string {
return map[string]string{
"Подсистема": "Subsystems",
"ОбщийМодуль": "CommonModules",
"ПараметрСеанса": "SessionParameters",
"Роль": "Roles",
"ОбщийРеквизит": "CommonAttributes",
"ПланОбмена": "ExchangePlans",
"КритерийОтбора": "FilterCriteria",
"ПодпискаНаСобытия": "EventSubscriptions",
"РегламентноеЗадание": "ScheduledJobs",
"ФункциональнаяОпция": "FunctionalOptions",
"ПараметрыФункциональнойОпции": "FunctionalOptionsParameters",
"ОпределяемыйТип": "DefinedTypes",
"ХранилищеНастроек": "SettingsStorages",
"ОбщаяФорма": "CommonForms",
"ОбщаяКоманда": "CommonCommands",
"ГруппаКоманды": "CommandGroups",
"ОбщийМакет": "CommonTemplates",
"ОбщаяКартинка": "CommonPictures",
"ПакетXDTO": "XDTOPackages",
"WebСервис": "WebServices",
"HTTPСервис": "HTTPServices",
"WSСсылка": "WSReferences",
"ЭлементСтиля": "StyleItems",
"Язык": "Languages",
"Константа": "Constants",
"Справочник": "Catalogs",
"НумераторДокументов": "DocumentNumerators",
"Документ": "Documents",
"ЖурналДокументов": "DocumentJournals",
"Перечисление": "Enums",
"Отчет": "Reports",
"Обработка": "DataProcessors",
"ПланВидовХарактеристик ": "ChartsOfCharacteristicTypes",
"РегистрСведений": "InformationRegisters",
"РегистрНакопления": "AccumulationRegisters",
"БизнесПроцесс": "BusinessProcesses",
"Задача": "Tasks",
}
}
func mapObjectNames() map[string]string {
return map[string]string{
"Команда": "Commands",
"Форма": "Forms",
"Макет": "Templates",
}
}
func mapModulesPath() map[string][]string {
return map[string][]string{
"МодульУправляемогоПриложения": {"Ext", "ManagedApplicationModule"},
"МодульСеанса": {"Ext", "SessionModule"},
"ОбщийМодуль": {"Ext", "Module"},
"HTTPСервис": {"Ext", "Module"},
"WebСервис": {"Ext", "Module"},
"СервисИнтеграции": {"Ext", "Module"},
"МодульМенеджера": {"Ext", "ManagerModule"},
"МодульОбъекта": {"Ext", "ObjectModule"},
"Форма": {"Ext", "Form", "Module"},
"Команда": {"Ext", "CommandModule"},
}
}

View File

@@ -0,0 +1,105 @@
package gitbsl
import (
"catcher/app/internal/testutil/logging"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsExternalModule(t *testing.T) {
tests := []struct {
input string
want bool
}{
{"ВнешняяОбработкаSomething", true},
{"ВнешнийОтчетSomething", true},
{"Обработка", false},
{"Отчет", false},
{"", false},
}
for _, tt := range tests {
got := IsExternalModule(tt.input)
assert.Equal(t, tt.want, got, "IsExternalModule(%q)", tt.input)
}
}
func TestAbsPath(t *testing.T) {
logger := &logging.TestLogger{}
tests := []struct {
input string
sourceCodeRoot string
wantPrefix string
wantErr bool
}{
{
input: "ОбщийМодуль.ОбменСSentry.Модуль",
sourceCodeRoot: "config",
wantPrefix: "config/CommonModules/ОбменСSentry/Ext/Module.bsl",
wantErr: false,
},
{
input: "Документ._Шаблон.Форма.ФормаДокумента.Форма",
sourceCodeRoot: "config",
wantPrefix: "config/Documents/_Шаблон/Forms/ФормаДокумента/Ext/Form/Module.bsl",
wantErr: false,
},
{
input: "Обработка.ОбменЭлектроннымиДокументамиСБанком.Команда.СписокЭД.МодульКоманды",
sourceCodeRoot: "config",
wantPrefix: "config/DataProcessors/ОбменЭлектроннымиДокументамиСБанком/Commands/СписокЭД/Ext/CommandModule.bsl",
wantErr: false,
},
{
input: "ОбщаяФорма.АдресУчастникаОбменаЭД.Форма",
sourceCodeRoot: "config",
wantPrefix: "config/CommonForms/АдресУчастникаОбменаЭД/Ext/Form/Module.bsl",
wantErr: false,
},
{
input: "Обработка.АРМПриемосдатчика.Форма.ПереченьСД.Форма",
sourceCodeRoot: "config",
wantPrefix: "config/DataProcessors/АРМПриемосдатчика/Forms/ПереченьСД/Ext/Form/Module.bsl",
wantErr: false,
},
{
input: "НеизвестныйТип.Объект",
sourceCodeRoot: "config",
wantPrefix: "",
wantErr: true,
},
}
for _, tt := range tests {
p := NewPath(tt.input, tt.sourceCodeRoot, logger)
got, err := p.AbsPath()
if tt.wantErr {
assert.Error(t, err, "AbsPath(%q) expected error", tt.input)
} else {
assert.NoError(t, err, "AbsPath(%q) unexpected error", tt.input)
assert.True(t, strings.HasPrefix(got, tt.wantPrefix), "AbsPath(%q) = %q, want prefix %q", tt.input, got, tt.wantPrefix)
}
}
}
func TestTranslateBaseType(t *testing.T) {
logger := &logging.TestLogger{}
p := NewPath("", "", logger)
tests := []struct {
input string
want string
}{
{"ОбщийМодуль", "CommonModules"},
{"Документ", "Documents"},
{"Неизвестно", ""},
}
for _, tt := range tests {
got := p.translateBaseType(tt.input, false)
assert.Equal(t, tt.want, got, "translateBaseType(%q)", tt.input)
}
}

View File

@@ -0,0 +1,79 @@
package iofiles
import (
"archive/zip"
"io"
"os"
"path/filepath"
"strings"
"github.com/cockroachdb/errors"
)
func SaveDataToFile(data []byte, src string) error {
return os.WriteFile(src, data, 0644)
}
func FileToByte(src string) ([]byte, error) {
f, err := os.Open(src)
if err != nil {
return nil, err
}
defer f.Close()
byteValue, err := io.ReadAll(f)
if err != nil {
return nil, err
}
return byteValue, nil
}
func Unzip(src string, dest string) error {
rcloser, err := zip.OpenReader(src)
if err != nil {
return err
}
defer rcloser.Close()
for _, f := range rcloser.File {
fpath := filepath.Join(dest, f.Name)
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
return errors.Wrap(err, "Не допустимый путь файла")
}
if f.FileInfo().IsDir() {
os.MkdirAll(fpath, os.ModePerm)
continue
}
if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
rc, err := f.Open()
if err != nil {
return err
}
_, err = io.Copy(outFile, rc)
outFile.Close()
rc.Close()
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,49 @@
package ioimage
import (
"bytes"
"image"
"image/jpeg"
"image/png"
"github.com/nfnt/resize"
"github.com/cockroachdb/errors"
)
func Compress(input []byte, percent uint) ([]byte, error) {
const op = "ioimage.Compress"
if percent == 0 || percent > 100 {
return nil, errors.Errorf("%s - percent должен быть в диапазоне 1-100, передано %v", op, percent)
}
img, format, err := image.Decode(bytes.NewReader(input))
if err != nil {
return nil, errors.WithMessagef(err, "%s - ошибка декодирования изображения", op)
}
origBounds := img.Bounds()
origWidth := uint(origBounds.Dx())
origHeight := uint(origBounds.Dy())
newWidth := origWidth * percent / 100
newHeight := origHeight * percent / 100
resizedImg := resize.Resize(newWidth, newHeight, img, resize.Lanczos3)
var buf bytes.Buffer
switch format {
case "jpeg":
err = jpeg.Encode(&buf, resizedImg, &jpeg.Options{Quality: 80})
case "png":
err = png.Encode(&buf, resizedImg)
default:
return nil, errors.Errorf("%s - неподдерживаемыq формат image: %s", op, format)
}
if err != nil {
return nil, errors.WithMessagef(err, "%s - ошибка кодирования изображения", op)
}
return buf.Bytes(), nil
}

View File

@@ -0,0 +1,52 @@
package logging
import (
"context"
"log/slog"
)
// MultiHandler отправляет записи сразу нескольким вложенным Handler-ам
type MultiHandler struct {
handlers []slog.Handler
}
func NewMultiHandler(handlers ...slog.Handler) *MultiHandler {
return &MultiHandler{handlers: handlers}
}
func (h *MultiHandler) Enabled(ctx context.Context, level slog.Level) bool {
for _, handler := range h.handlers {
if handler.Enabled(ctx, level) {
return true
}
}
return false
}
func (h *MultiHandler) Handle(ctx context.Context, r slog.Record) error {
var firstErr error
for _, handler := range h.handlers {
if handler.Enabled(ctx, r.Level) {
if err := handler.Handle(ctx, r); err != nil && firstErr == nil {
firstErr = err
}
}
}
return firstErr
}
func (h *MultiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
newHandlers := make([]slog.Handler, len(h.handlers))
for i, handler := range h.handlers {
newHandlers[i] = handler.WithAttrs(attrs)
}
return &MultiHandler{handlers: newHandlers}
}
func (h *MultiHandler) WithGroup(name string) slog.Handler {
newHandlers := make([]slog.Handler, len(h.handlers))
for i, handler := range h.handlers {
newHandlers[i] = handler.WithGroup(name)
}
return &MultiHandler{handlers: newHandlers}
}

View File

@@ -0,0 +1,160 @@
package logging
import (
"catcher/app/internal/config"
"fmt"
"log"
"os"
"path/filepath"
"log/slog"
"github.com/lmittmann/tint"
"github.com/mattn/go-colorable"
)
// Logger — интерфейс логгера, используемый в проекте
type Logger interface {
Debug(msg string, attrs ...slog.Attr)
Info(msg string, attrs ...slog.Attr)
Warn(msg string, attrs ...slog.Attr)
Error(msg string, attrs ...slog.Attr)
// Вспомогательные методы для создания атрибутов
Err(err error) slog.Attr
Op(value string) slog.Attr
Str(key, value string) slog.Attr
}
var logger *slog.Logger
type defaultlogger struct {
*slog.Logger
}
var levelMap = map[int]slog.Level{
5: slog.LevelDebug,
4: slog.LevelInfo,
3: slog.LevelWarn,
2: slog.LevelError,
}
func GetLogger() Logger {
return NewLogger(logger)
}
func Initlogger(c *config.Config) Logger {
var handler slog.Handler
level, ok := levelMap[c.Log.Level]
if !ok {
level = slog.LevelInfo
}
if !c.Log.OutputInFile {
// Вывод в stdout с текстовым форматом
handler = tint.NewHandler(colorable.NewColorable(os.Stderr), &tint.Options{
Level: level,
TimeFormat: "15:04:05",
AddSource: false,
NoColor: false,
})
} else {
// Вывод в файл с JSON форматом
fileName := "app.log"
file, err := GetOutputLogFile(c.WorkingDir, c.Log.Dir, fileName)
if err != nil {
log.Printf("Не удалось открыть файл логов %q, используется стандартный stderr\n%v", fileName, err)
handler = slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
Level: level,
AddSource: false,
})
} else {
handler = slog.NewJSONHandler(file, &slog.HandlerOptions{
Level: level,
AddSource: false,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
// Преобразуем время в строку с нужным форматом
t := a.Value.Time()
formatted := t.Format("2006-01-02 15:04:05")
return slog.String(a.Key, formatted)
}
return a
},
})
}
}
if c.Sentry.Use {
multiHandler := NewMultiHandler(handler, SentryHandler())
logger = slog.New(multiHandler)
} else {
logger = slog.New(handler)
}
return NewLogger(logger)
}
// NewLogger создаёт Logger из slog.Logger
func NewLogger(slogger *slog.Logger) Logger {
return &defaultlogger{slogger}
}
func (l *defaultlogger) Debug(msg string, attrs ...slog.Attr) {
l.Logger.Debug(msg, convertAttrsToAny(attrs)...)
}
func (l *defaultlogger) Info(msg string, attrs ...slog.Attr) {
l.Logger.Info(msg, convertAttrsToAny(attrs)...)
}
func (l *defaultlogger) Warn(msg string, attrs ...slog.Attr) {
l.Logger.Warn(msg, convertAttrsToAny(attrs)...)
}
func (l *defaultlogger) Error(msg string, attrs ...slog.Attr) {
l.Logger.Error(msg, convertAttrsToAny(attrs)...)
}
func (l *defaultlogger) Err(err error) slog.Attr {
return slog.Any("error", err)
}
func (l *defaultlogger) Op(value string) slog.Attr {
return slog.Attr{
Key: "op",
Value: slog.StringValue(value),
}
}
func (l *defaultlogger) Str(key, value string) slog.Attr {
return slog.Attr{
Key: key,
Value: slog.StringValue(value),
}
}
func GetOutputLogFile(workingDir, logDir, fileName string) (*os.File, error) {
fullLogDir := filepath.Join(workingDir, logDir)
err := os.MkdirAll(fullLogDir, 0755)
if err != nil {
return nil, err
}
path := fmt.Sprintf("%s/%s", fullLogDir, fileName)
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
return file, err
}
func convertAttrsToAny(attrs []slog.Attr) []any {
res := make([]any, len(attrs))
for i, v := range attrs {
res[i] = v
}
return res
}

View File

@@ -0,0 +1,42 @@
package logging
import (
"catcher/app/internal/config"
"log/slog"
"time"
"github.com/getsentry/sentry-go"
sentryslog "github.com/getsentry/sentry-go/slog"
)
func SentryHandler() slog.Handler {
return sentryslog.Option{
Level: slog.LevelWarn,
AddSource: true,
}.NewSentryHandler()
}
func SentryClientOptions(c *config.Config) sentry.ClientOptions {
var environment string
if c.UseDebug() {
environment = "Debug"
} else {
environment = "Production"
}
sentrySyncTransport := sentry.NewHTTPSyncTransport()
sentrySyncTransport.Timeout = time.Second * 3
return sentry.ClientOptions{
Transport: sentrySyncTransport,
Dsn: c.Sentry.Dsn,
Release: c.ProjectName + "@" + c.Version,
Environment: environment,
Debug: c.UseDebug(),
AttachStacktrace: c.Sentry.AttachStacktrace,
TracesSampleRate: c.Sentry.TracesSampleRate,
EnableTracing: c.Sentry.EnableTracing,
}
}

View File

@@ -0,0 +1,49 @@
package times
import (
"encoding/json"
"fmt"
"time"
)
const format = "2006-01-02T15:04:05"
// Кастомный тип для даты с поддержкой нескольких форматов
type TimeTZ struct {
time.Time
}
func (tz *TimeTZ) UnmarshalJSON(b []byte) error {
str := string(b)
if len(str) < 2 {
return fmt.Errorf("invalid date: %s", str)
}
str = str[1 : len(str)-1] // убираем кавычки
layouts := []string{
time.RFC3339,
format,
}
var err error
for _, layout := range layouts {
var t time.Time
t, err = time.Parse(layout, str)
if err == nil {
tz.Time = t
return nil
}
}
return fmt.Errorf("cannot parse date: %s", str)
}
type UnixTime time.Time
func (t *UnixTime) UnmarshalJSON(b []byte) error {
var ts int64
if err := json.Unmarshal(b, &ts); err != nil {
return err
}
*t = UnixTime(time.Unix(ts, 0))
return nil
}

View File

@@ -0,0 +1,55 @@
package models
import (
"catcher/app/internal/lib/times"
"github.com/getsentry/sentry-go"
)
type EventID string
type Event struct {
sentry.Event
Timestamp times.UnixTime `json:"timestamp"`
Exception Exception `json:"exception"`
Request any `json:"request"`
}
type Exception struct {
Values []ExceptionValue `json:"values"`
}
type ExceptionValue struct {
Type string `json:"type"`
Value string `json:"value"`
Stacktrace Stacktrace `json:"stacktrace"`
}
type Stacktrace struct {
Frames []Frame `json:"frames"`
}
type Frame struct {
Lineno int `json:"lineno"`
Function string `json:"function"`
Filename string `json:"filename"`
Module string `json:"module"`
ModuleAbs string `json:"module_abs"`
ContextLine string `json:"context_line"`
InApp bool `json:"in_app"`
AbsPath string `json:"abs_path"`
StackStart bool `json:"stack_start"`
}
type SendEventResult struct {
ID string
EventID *EventID
}
func (e *EventID) IsEmpty() bool {
return e == nil || *e == ""
}
func (e EventID) String() string {
return string(e)
}

View File

@@ -0,0 +1,30 @@
package models
import (
"catcher/app/internal/config"
"catcher/app/internal/lib/caching"
"catcher/app/internal/lib/logging"
"context"
)
const (
DirWeb = "web"
DirTemp = "temp"
)
type CtxKey string
type AppContext struct {
Ctx context.Context
Config *config.Config
Cacher caching.Cacher
Logger logging.Logger
}
func NewAppContext(ctx context.Context, c *config.Config, cacher caching.Cacher, loger logging.Logger) AppContext {
return AppContext{
Ctx: ctx,
Config: c,
Cacher: cacher,
Logger: loger}
}

View File

@@ -0,0 +1,38 @@
package models
type RegistryInfo struct {
NeedSendReport bool `json:"needSendReport"`
UserMessage string `json:"userMessage"`
DumpType int `json:"dumpType"`
}
type RegistryInput struct {
ConfigName string `json:"configName" binding:"required"`
ConfigHash string `json:"configHash"`
ConfigVersion string `json:"configVersion"`
AppStackHash string `json:"appStackHash"`
ClientStackHash string `json:"clientStackHash"`
PlatformType string `json:"platformType"`
AppName string `json:"appName"`
AppVersion string `json:"appVersion"`
PlatformInterfaceLanguageCode string `json:"platformInterfaceLanguageCode"`
ConfigurationInterfaceLanguageCode string `json:"configurationInterfaceLanguageCode"`
ErrorCategories []string `json:"ErrorCategories"`
ClientID string `json:"clientID"`
ReportID string `json:"reportID"`
}
type RegistryPushReportInput struct {
ID string
Data []byte
}
type RegistryPushReportResult struct {
ID string
EventID *EventID
}
func NewRegistryInput() RegistryInput {
return RegistryInput{
ConfigName: "none",
}
}

View File

@@ -0,0 +1,132 @@
package models
import (
"catcher/app/internal/config"
"time"
)
// https://its.1c.ru/db/v8327doc#bookmark:dev:TI000002558
type Repport struct {
Time time.Time `json:"time"`
Id string `json:"id"`
ClientInfo struct {
PlatformType string `json:"platformType"`
AppVersion string `json:"appVersion"`
AppName string `json:"appName"`
SystemInfo struct {
OsVersion string `json:"osVersion"`
FullRAM int64 `json:"fullRAM"`
FreeRAM int64 `json:"freeRAM"`
Processor string `json:"processor"`
Useragent string `json:"useragent"`
ClientID string `json:"clientID"`
} `json:"systemInfo"`
} `json:"clientInfo"`
SessionInfo struct {
UserName string `json:"userName"`
DataSeparation string `json:"dataSeparation"`
PlatformInterfaceLanguageCode string `json:"platformInterfaceLanguageCode"`
ConfigurationInterfaceLanguageCode string `json:"configurationInterfaceLanguageCode"`
LocaleCode string `json:"localeCode"`
UserInfo UserInfo // Добавляются сервисом userinfo
} `json:"sessionInfo"`
InfoBaseInfo struct {
LocaleCode string `json:"localeCode"`
} `json:"infoBaseInfo"`
ServerInfo struct {
PlatformType string `json:"platformType"`
AppVersion string `json:"appVersion"`
Dbms string `json:"dbms"`
} `json:"serverInfo"`
ConfigInfo struct {
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
CompatibilityMode string `json:"compatibilityMode"`
Hash string `json:"hash"`
ChangeEnabled bool `json:"changeEnabled"`
Extentions []extention `json:"extentions"`
DisabledExtentions []extention `json:"disabledExtentions"`
} `json:"configInfo"`
ErrorInfo struct {
UserDescription string `json:"userDescription"`
SystemErrorInfo struct {
ClientStack string `json:"clientStack"`
ClientStackHash string `json:"clientStackHash"`
ServerStack string `json:"serverStack"`
ServerStackHash string `json:"serverStackHash"`
SystemCrash bool `json:"systemCrash"`
} `json:"systemErrorInfo"`
ApplicationErrorInfo struct {
Errors []errorsHeap `json:"errors"`
Stack []stackHeap `json:"stack"`
StackHash string `json:"stackHash"`
} `json:"applicationErrorInfo"`
} `json:"errorInfo"`
Screenshot struct {
File string `json:"file"`
} `json:"screenshot"`
AdditionalInfo string `json:"additionalInfo"`
AdditionalData string `json:"additionalData"`
AdditionalFiles []string `json:"additionalFiles"`
Dump struct {
TypeDump string `json:"type"`
File string `json:"file"`
ReasonForNoDump struct {
GenericFailure string `json:"genericFailure"`
UserRefused string `json:"userRefused"`
InsufficientResources string `json:"insufficientResources"`
} `json:"reasonForNoDump"`
} `json:"dump"`
}
type extention [2]string
type errorsHeap [2]any
type stackHeap [3]any
type FileData struct {
Name string
Data []byte
}
type RepportData struct {
ID string
Prj config.Project
Data *Repport
Files []FileData
Src string
SrcDirFiles string
}
func (r Repport) ProjectByConfig(c *config.Config) (config.Project, error) {
var prj config.Project
var err error
additionalInfo := r.AdditionalInfo
if additionalInfo != "" {
prj, err = c.ProjectById(additionalInfo)
} else {
prj, err = c.ProjectByName(r.ConfigInfo.Name)
}
return prj, err
}
type UserInfo struct {
Id string
City string
Branch string
Position string
Started time.Time
SessionInfo struct {
IP string
Device string
Session int
Connection int
}
}
func (u UserInfo) Empty() bool {
return u == UserInfo{}
}

View File

@@ -0,0 +1,138 @@
package normalize
import (
"regexp"
"strings"
)
func Id(id string) string {
return strings.ReplaceAll(id, "-", "")
}
func Release(id, version string) string {
return id + "@" + version
}
func Message(input string) string {
message := removeBraces(input)
message = strings.TrimSpace(message)
return message
}
// 192.168.1.2 , 192.168.1.3
func Ip(input string) string {
var res string
parts := strings.Split(input, ",")
if len(parts) > 0 {
res = parts[0]
} else {
res = input
}
return strings.TrimSpace(res)
}
// Пример строки
// Infostart Toolkit PROF (2023.4.08)
func SplitString(input, sStart, sEnd string) [2]string {
var result [2]string
// Ищем любые символы (кроме скобок) внутри скобок
// \([^)]*\)
re := regexp.MustCompile(`\` + sStart + `[^` + sEnd + `]*\` + sEnd)
first := re.ReplaceAllString(input, "")
first = strings.TrimSpace(first)
second := re.FindString(input)
second = strings.ReplaceAll(second, sStart, "")
second = strings.ReplaceAll(second, sEnd, "")
second = strings.TrimSpace(second)
result[0] = first
result[1] = second
return result
}
func RemoveModuleNameSuffix(s string) string {
suffixs := [2]string{".Модуль", ".Форма"}
for _, suffix := range suffixs {
s = strings.TrimSuffix(s, suffix)
}
return s
}
func removeBraces(input string) string {
// Регулярное выражение для нахождения всех вхождений {любые_символы_включая_переносы_строк}
// re := regexp.MustCompile(`\{[^{}]*\}:|\}`)
re := regexp.MustCompile(`\{[^{}]*\}:?|\}`)
// Заменяем все совпадения на пустую строку
result := re.ReplaceAllString(input, "")
return result
}
// RemoveFromSecondBrace - удаляет полный стек из строки
// удаляет из строки все символы начиная со второй '{' до первой пустой строки (или до конца текста, если пустой строки нет)
func RemoveFromSecondBrace(input string, addBrace bool) string {
// Найти все вхождения '{'
re := regexp.MustCompile(`\{`)
indices := re.FindAllStringIndex(input, -1)
if len(indices) < 2 {
return input // Если меньше двух '{', возвращаем исходный текст
}
secondBraceIdx := indices[1][0]
// Получаем подстроку начиная со второго '{'
substr := input[secondBraceIdx:]
// Разбиваем подстроку на строки для поиска пустой строки
lines := strings.Split(substr, "\n")
emptyLineIdx := -1
for i, line := range lines {
if strings.TrimSpace(line) == "" {
emptyLineIdx = i
break
}
}
result := input[:secondBraceIdx]
if emptyLineIdx != -1 {
// Если пустая строка найдена, возвращаем текст до второго '{' + текст после пустой строки
// Добавляем "\n" между частями, если нужно
afterEmpty := strings.Join(lines[emptyLineIdx+1:], "\n")
if len(afterEmpty) > 0 && !strings.HasPrefix(afterEmpty, "\n") {
afterEmpty = "\n" + afterEmpty
}
// Если подстрока заканчивается запятой, удаляем её
// Добавляем закрывающую фигурную скобку
if addBrace {
if strings.HasSuffix(result, "\n") {
result = strings.TrimSuffix(result, "\n")
result += "}\n"
} else {
result += "}"
}
}
result = result + afterEmpty
}
result = strings.TrimSpace(result)
// Если пустой строки нет, возвращаем текст до второго '{' (удаляем всё от второго '{' до конца)
return result
}

View File

@@ -0,0 +1,120 @@
package normalize
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestId(t *testing.T) {
input := "123-456-789"
expected := "123456789"
got := Id(input)
assert.Equal(t, expected, got, "Id(%q) should return %q", input, expected)
}
func TestRelease(t *testing.T) {
id := "module"
version := "1.0"
expected := "module@1.0"
got := Release(id, version)
assert.Equal(t, expected, got, "Release(%q, %q) should return %q", id, version, expected)
}
func TestMessage(t *testing.T) {
input := " {test} message {test} "
expected := "message"
got := Message(input)
assert.Equal(t, expected, got, "Message(%q) should return %q", input, expected)
}
func TestIp(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"192.168.0.1, 10.0.0.1", "192.168.0.1"},
{" 127.0.0.1 , localhost", "127.0.0.1"},
{"255.255.255.255", "255.255.255.255"},
{" 10.10.10.10 ,", "10.10.10.10"},
{" , 1.1.1.1", ""},
{"", ""},
}
for _, tt := range tests {
result := Ip(tt.input)
assert.Equal(t, tt.expected, result, "Ip(%q) должен вернуть %q", tt.input, tt.expected)
}
}
func TestSplitString(t *testing.T) {
input := "Infostart Toolkit PROF (2023.4.08)"
sStart := "("
sEnd := ")"
expected := [2]string{"Infostart Toolkit PROF", "2023.4.08"}
got := SplitString(input, sStart, sEnd)
assert.Equal(t, expected, got, "SplitString(%q, %q, %q) should return %v", input, sStart, sEnd, expected)
}
func TestRemoveModuleNameSuffix(t *testing.T) {
cases := []struct {
input string
expected string
}{
{"example.Модуль", "example"},
{"example.Форма", "example"},
{"example", "example"},
}
for _, c := range cases {
got := RemoveModuleNameSuffix(c.input)
assert.Equal(t, c.expected, got, "RemoveModuleNameSuffix(%q) should return %q", c.input, c.expected)
}
}
func TestRemoveBraces(t *testing.T) {
input := "{test}: some text }"
expected := " some text "
got := removeBraces(input)
assert.Equal(t, expected, got, "removeBraces(%q) should return %q", input, expected)
}
func TestRemoveFromSecondBrace(t *testing.T) {
input := `Ошибка при вызове метода контекста (ПредопределенноеЗначение)
{Обработка.ОтчетПоРейсу.МодульОбъекта(115)}:ОценкаПроизводительностиВызовСервераПолныеПрава.ЗакончитьЗамерВремениНаСервере(ПредопределенноеЗначение("Справочник.КлючевыеОперации.Рейс_АД_Отчет_Услуги_Сформировать")
{Обработка.ОтчетПоРейсу.МодульОбъекта(32)}:СформироватьОтчетПоУслугамРейса(ВидОтчета, Результат, ПараметрыСКД, ДополнительныеПараметры);
{Справочник.Рейсы.МодульМенеджера(1672)}:ОбработкаОтчета.СформироватьОтчет(ВидОтчета, Результат, ПараметрыСКД, ДополнительныеПараметры);
{Справочник.Рейсы.Форма.ФормаЭлементаАДКД.Форма(3074)}:Справочники.Рейсы.СформироватьОтчет("УслугиРейса", ТабличныйДокументУслуги, ПараметрыСКД, ДополнительныеПараметры);
{Справочник.Рейсы.Форма.ФормаЭлементаАДКД.Форма(3026)}:СформироватьУслуги();
[ОшибкаВоВремяВыполненияВстроенногоЯзыка]
по причине:
Предопределенный элемент не существует`
expected := `Ошибка при вызове метода контекста (ПредопределенноеЗначение)
{Обработка.ОтчетПоРейсу.МодульОбъекта(115)}:ОценкаПроизводительностиВызовСервераПолныеПрава.ЗакончитьЗамерВремениНаСервере(ПредопределенноеЗначение("Справочник.КлючевыеОперации.Рейс_АД_Отчет_Услуги_Сформировать")}
[ОшибкаВоВремяВыполненияВстроенногоЯзыка]
по причине:
Предопределенный элемент не существует`
result := RemoveFromSecondBrace(input, true)
assert.Equal(t, expected, result, "Строка должна быть обрезана начиная со второй '{' до пустой строки, запятая удалена, добавлена '}'")
// Тест: если меньше двух '{', строка не меняется
input2 := "Текст без фигурных скобок\n"
expected2 := input2
result2 := RemoveFromSecondBrace(input2, true)
assert.Equal(t, expected2, result2, "Если меньше двух '{', строка не меняется")
// Тест: если нет пустой строки после второй '{', и есть запятая в конце
input3 := "abc {first} def {second}, xyz"
expected3 := "abc {first} def"
result3 := RemoveFromSecondBrace(input3, false)
assert.Equal(t, expected3, result3, "Если пустой строки нет после второй '{', удаляем запятую и добавляем '}'")
// Тест: если нет пустой строки после второй '{', и запятая отсутствует
input4 := "abc {first} def {second} xyz"
expected4 := "abc {first} def"
result4 := RemoveFromSecondBrace(input4, false)
assert.Equal(t, expected4, result4, "Если пустой строки нет после второй '{', и запятая отсутствует, просто добавляем '}'")
}

View File

@@ -0,0 +1,86 @@
package sentryhub
import (
"catcher/app/internal/config"
"catcher/app/internal/models"
"sync"
"time"
"github.com/getsentry/sentry-go"
)
type Hub struct {
*sentry.Hub
appCtx models.AppContext
}
type project struct {
name string
id string
}
type hubsmap map[project]Hub
var hubs hubsmap
var hubsMu sync.RWMutex
func init() {
hubs = make(hubsmap)
}
func New(prj config.Project, appCtx models.AppContext) (*Hub, error) {
c, err := sentry.NewClient(clientOptions(prj, appCtx))
if err != nil {
return nil, err
}
h := sentry.NewHub(c, sentry.NewScope())
hub := Hub{h, appCtx}
hubsMu.Lock()
hubs[toProject(prj)] = hub
hubsMu.Unlock()
return &hub, nil
}
func Get(prj config.Project, appCtx models.AppContext) (*Hub, error) {
hubsMu.RLock()
hub, ok := hubs[toProject(prj)]
hubsMu.RUnlock()
if ok {
return &hub, nil
}
return New(prj, appCtx)
}
func toProject(prj config.Project) project {
return project{
name: prj.Name,
id: prj.Name,
}
}
func clientOptions(prj config.Project, appCtx models.AppContext) sentry.ClientOptions {
sentrySyncTransport := sentry.NewHTTPSyncTransport()
sentrySyncTransport.Timeout = time.Second * 3
return sentry.ClientOptions{
Transport: sentrySyncTransport,
Dsn: prj.Sentry.Dsn,
Release: prj.Release(),
Environment: prj.Sentry.Environment,
Debug: appCtx.Config.UseDebug(),
AttachStacktrace: false,
EnableTracing: false,
Integrations: func([]sentry.Integration) []sentry.Integration {
return make([]sentry.Integration, 0)
},
}
}

View File

@@ -0,0 +1,72 @@
package http
import (
"catcher/app/internal/models"
"context"
"fmt"
"net/http"
"sync"
"time"
"github.com/cockroachdb/errors"
)
type Server struct {
models.AppContext
httpServer *http.Server
stop chan struct{}
stopOnce sync.Once
}
func New(appCtx models.AppContext) *Server {
return &Server{AppContext: appCtx}
}
func (s *Server) Run(port string, handler http.Handler) error {
s.httpServer = &http.Server{
Addr: ":" + port,
Handler: handler,
MaxHeaderBytes: 1 << 20, // 1 MB
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
s.stop = make(chan struct{})
errCh := make(chan error)
go func() {
defer func() {
if r := recover(); r != nil {
errCh <- fmt.Errorf("panic recovered: %v", r)
}
}()
if err := s.httpServer.ListenAndServe(); err != nil {
errCh <- errors.Wrap(err, "http.ListenAndServe")
}
}()
select {
case err := <-errCh:
return err
case <-s.stop:
return nil
}
}
func (s *Server) Shutdown(ctx context.Context) error {
defer s.stopOnce.Do(func() {
close(s.stop)
})
err := s.httpServer.Shutdown(ctx)
if err != nil {
err = errors.Wrap(err, "http.Shutdown")
}
return err
}

View File

@@ -0,0 +1,100 @@
package server
import (
"catcher/app/internal/handler"
"catcher/app/internal/lib/logging"
"catcher/app/internal/models"
"catcher/app/internal/service"
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"time"
svc "github.com/kardianos/service"
"github.com/cockroachdb/errors"
)
var errServerClosed = errors.New("http.ListenAndServe: http: Server closed")
type Program struct {
models.AppContext
Srv Srv
}
type Srv interface {
Run(port string, handler http.Handler) error
Shutdown(ctx context.Context) error
}
func NewProgram(srv Srv, appCtx models.AppContext) *Program {
return &Program{Srv: srv,
AppContext: appCtx}
}
func (p *Program) Start(s svc.Service) error {
// Запускаем в отдельной горутине, чтобы не блокировать Start
i := "Starting a web-server on port"
port := p.Config.Server.Port
p.Logger.Info(i,
p.Logger.Str("port", port),
p.Logger.Str("version", p.Config.Version))
if p.Config.Log.OutputInFile {
fmt.Printf("%s: %s\n", i, port)
}
go p.Run()
return nil
}
func (p *Program) Run() {
appCtx := p.AppContext
service := service.NewService(appCtx)
handlers := handler.NewHandler(service, appCtx)
logger := logging.GetLogger()
port := appCtx.Config.ServerPort()
err := os.MkdirAll(filepath.Join(appCtx.Config.WorkingDir, models.DirWeb, models.DirTemp), 0755)
if err != nil {
logger.Error("Ошибка создание временных каталогов",
logger.Err(err))
os.Exit(1)
}
if err := p.Srv.Run(port, handlers.InitHandler()); err != nil && err.Error() != errServerClosed.Error() {
logger.Error("Error running server",
logger.Err(err))
os.Exit(1)
}
}
func (p *Program) Stop(s svc.Service) error {
appCtx := p.AppContext
appCtx.Logger.Debug("Server Shutting down")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := p.Srv.Shutdown(ctx); err != nil {
appCtx.Logger.Error("Error on server shutting down",
appCtx.Logger.Err(err))
return err
}
i := "Server is stopped"
appCtx.Logger.Info(i)
if p.Config.Log.OutputInFile {
fmt.Printf("%s\n", i)
}
return nil
}

View File

@@ -0,0 +1,121 @@
package nonexept
import (
"catcher/app/internal/lib/logging"
"context"
"encoding/json"
"io"
"log/slog"
"net/http"
"net/url"
"time"
"github.com/cockroachdb/errors"
)
const method = "nonexception"
var ErrBadRequest = errors.New("bad request")
type Response struct {
Result bool `json:"result" binding:"required"`
Exeptions []string `json:"exeptions"`
}
type Service struct {
url string
timeOut int
credintials Credintials
logger logging.Logger
}
type Credintials struct {
userName string
password string
}
func NewService(url string, timeOut int, creds Credintials, logger logging.Logger) Service {
return Service{
url: url,
timeOut: timeOut,
credintials: creds,
logger: logger,
}
}
func NewCredintials(userName, password string) Credintials {
return Credintials{
userName: userName,
password: password,
}
}
func (s Service) Get(ctx context.Context) ([]string, error) {
const op = "nonexept.Get"
ctxTimeOut, cancel := context.WithTimeout(ctx, time.Duration(s.timeOut)*time.Second)
defer cancel()
path, _ := url.JoinPath(s.url, method)
req, err := http.NewRequestWithContext(ctxTimeOut, http.MethodGet, path, nil)
if err != nil {
s.logger.Error("Обшибка создяния контекста nonexept",
s.logger.Err(err),
s.logger.Op(op))
return nil, errors.New("nonexept.http.NewRequestWithContext")
}
req.SetBasicAuth(s.credintials.userName, s.credintials.password)
resp, err := http.DefaultClient.Do(req)
if err != nil {
s.logger.Error("Обшибка получения nonexept",
s.logger.Err(err),
s.logger.Op(op))
return nil, errors.New("nonexept.http.DefaultClient.Do")
}
defer func() {
errBodyClose := resp.Body.Close()
if errBodyClose != nil {
s.logger.Error("ошибка закрытия response body",
s.logger.Err(errBodyClose))
}
}()
body, err := io.ReadAll(resp.Body)
if err != nil {
s.logger.Error("Обшибка чтения nonexept",
s.logger.Err(err),
s.logger.Op(op))
return nil, errors.New("nonexept.io.ReadAll")
}
// Проверяем статус ответа
if resp.StatusCode != http.StatusOK {
s.logger.Error("Ошибка получения nonexept",
s.logger.Str("url", s.url),
s.logger.Str("status", resp.Status),
slog.Int("status_code", resp.StatusCode),
s.logger.Str("body", string(body)),
s.logger.Op(op))
return nil, ErrBadRequest
}
// Парсим ответ
var res Response
if err = json.Unmarshal(body, &res); err != nil {
s.logger.Error("Ошибка парсинга nonexept",
s.logger.Err(err),
s.logger.Op(op))
return nil, errors.New("nonexept.json.Unmarshal")
}
s.logger.Debug("Получен список пропускаемых exeptions",
s.logger.Str("url", s.url),
s.logger.Op(op))
return res.Exeptions, nil
}

View File

@@ -0,0 +1,96 @@
package nonexept
import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"catcher/app/internal/testutil/logging"
"github.com/stretchr/testify/assert"
)
// Хелпер для создания сервиса с тестовым http.Client
func newTestService(serverURL string) Service {
return NewService(
serverURL,
1, // timeout
NewCredintials("user", "pass"),
&logging.TestLogger{},
)
}
func TestService_Get_Success(t *testing.T) {
// Мокаем сервер
handler := func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/nonexception", r.URL.Path)
user, pass, ok := r.BasicAuth()
assert.True(t, ok)
assert.Equal(t, "user", user)
assert.Equal(t, "pass", pass)
w.WriteHeader(http.StatusOK)
io.WriteString(w, `{"result":true,"exeptions":["A","B"]}`)
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
svc := newTestService(server.URL)
ctx := context.Background()
exeptions, err := svc.Get(ctx)
assert.NoError(t, err)
assert.Equal(t, []string{"A", "B"}, exeptions)
}
func TestService_Get_BadStatus(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
io.WriteString(w, "bad request")
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
svc := newTestService(server.URL)
ctx := context.Background()
exeptions, err := svc.Get(ctx)
assert.ErrorIs(t, err, ErrBadRequest)
assert.Nil(t, exeptions)
}
func TestService_Get_InvalidJSON(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
io.WriteString(w, "not a json")
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
svc := newTestService(server.URL)
ctx := context.Background()
exeptions, err := svc.Get(ctx)
assert.Error(t, err)
assert.Nil(t, exeptions)
}
func TestService_Get_Timeout(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
w.WriteHeader(http.StatusOK)
io.WriteString(w, `{"result":true,"exeptions":[]}`)
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
svc := NewService(server.URL, 1, NewCredintials("user", "pass"), &logging.TestLogger{})
ctx := context.Background()
exeptions, err := svc.Get(ctx)
assert.Error(t, err)
assert.Nil(t, exeptions)
}

View File

@@ -0,0 +1,154 @@
package userinfo
import (
"catcher/app/internal/lib/logging"
"catcher/app/internal/lib/times"
"catcher/app/internal/models"
"context"
"encoding/json"
"io"
"log/slog"
"net/http"
"net/url"
"strings"
"time"
"github.com/jinzhu/copier"
"github.com/cockroachdb/errors"
)
const method = "userInfo"
var ErrBadRequest = errors.New("bad request")
type Response struct {
Result bool `json:"result" binding:"required"`
UserInfo struct {
Id string `json:"id"`
City string `json:"city"`
Branch string `json:"branch"`
Position string `json:"position"`
Started times.TimeTZ `json:"started"`
SessionInfo struct {
IP string `json:"IP"`
Device string `json:"device"`
Session int `json:"session"`
Connection int `json:"connection"`
} `json:"session_info"`
} `json:"userinfo"`
}
type Service struct {
url string
timeOut int
credintials Credintials
logger logging.Logger
}
type Credintials struct {
userName string
password string
}
func NewService(url string, timeOut int, creds Credintials, logger logging.Logger) Service {
return Service{
url: url,
timeOut: timeOut,
credintials: creds,
logger: logger,
}
}
func NewCredintials(userName, password string) Credintials {
return Credintials{
userName: userName,
password: password,
}
}
func (s Service) Get(ctx context.Context, input string) (*models.UserInfo, error) {
const op = "userinfo.Get"
ctxTimeOut, cancel := context.WithTimeout(ctx, time.Duration(s.timeOut)*time.Second)
defer cancel()
path, _ := url.JoinPath(s.url, method)
req, err := http.NewRequestWithContext(ctxTimeOut, http.MethodGet, path, nil)
if err != nil {
s.logger.Error("Обшибка создяния контекста userinfo",
s.logger.Err(err),
s.logger.Str("name", input),
s.logger.Op(op))
return nil, errors.New("userinfo.http.NewRequestWithContext")
}
req.SetBasicAuth(s.credintials.userName, s.credintials.password)
q := req.URL.Query()
q.Add("username", input)
encoded := q.Encode()
encoded = strings.ReplaceAll(encoded, "+", "%20") // Возвращаем назад пробелы
req.URL.RawQuery = encoded
resp, err := http.DefaultClient.Do(req)
if err != nil {
s.logger.Error("Обшибка получения userinfo",
s.logger.Err(err),
s.logger.Str("name", input),
s.logger.Op(op))
return nil, errors.New("userinfo.http.DefaultClient.Do")
}
defer func() {
errBodyClose := resp.Body.Close()
if errBodyClose != nil {
s.logger.Error("ошибка закрытия response body",
s.logger.Str("name", input),
s.logger.Str("name", input),
s.logger.Err(errBodyClose))
}
}()
body, err := io.ReadAll(resp.Body)
if err != nil {
s.logger.Error("Обшибка чтения userinfo",
s.logger.Err(err),
s.logger.Str("name", input),
s.logger.Op(op))
return nil, errors.New("userinfo.io.ReadAll")
}
// Проверяем статус ответа
if resp.StatusCode != http.StatusOK {
s.logger.Error("Ошибка получения userinfo",
s.logger.Str("url", s.url),
s.logger.Str("status", resp.Status),
slog.Int("status_code", resp.StatusCode),
s.logger.Str("body", string(body)),
s.logger.Op(op))
return nil, ErrBadRequest
}
// Парсим ответ
var res Response
if err = json.Unmarshal(body, &res); err != nil {
s.logger.Error("Ошибка парсинга userinfo",
s.logger.Err(err),
s.logger.Str("name", input),
s.logger.Op(op))
return nil, errors.New("userinfo.json.Unmarshal")
}
var resInfo models.UserInfo
if res.Result {
copier.Copy(&resInfo, &res.UserInfo)
resInfo.Started = res.UserInfo.Started.Time
} else {
s.logger.Warn("userinfo - нет данных пользователя",
s.logger.Str("name", input),
s.logger.Op(op))
}
return &resInfo, nil
}

View File

@@ -0,0 +1,92 @@
package userinfo
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"catcher/app/internal/lib/times"
"catcher/app/internal/testutil/logging"
"github.com/stretchr/testify/assert"
)
func TestService_Get_Success(t *testing.T) {
// Подготовка тестового ответа
resp := Response{
Result: true,
}
resp.UserInfo.Id = "123"
resp.UserInfo.City = "Москва"
resp.UserInfo.Branch = "Центральный"
resp.UserInfo.Position = "Инженер"
resp.UserInfo.Started = times.TimeTZ{Time: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)}
resp.UserInfo.SessionInfo.IP = "127.0.0.1"
resp.UserInfo.SessionInfo.Device = "PC"
resp.UserInfo.SessionInfo.Session = 42
resp.UserInfo.SessionInfo.Connection = 1
body, _ := json.Marshal(resp)
// Имитация внешнего сервера
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/userInfo", r.URL.Path)
assert.Equal(t, "testuser", r.URL.Query().Get("username"))
w.WriteHeader(http.StatusOK)
w.Write(body)
}))
defer server.Close()
creds := NewCredintials("user", "pass")
svc := NewService(server.URL, 2, creds, &logging.TestLogger{})
ctx := context.Background()
info, err := svc.Get(ctx, "testuser")
assert.NoError(t, err)
assert.NotNil(t, info)
assert.Equal(t, "123", info.Id)
assert.Equal(t, "Москва", info.City)
assert.Equal(t, "Центральный", info.Branch)
assert.Equal(t, "Инженер", info.Position)
assert.Equal(t, time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), info.Started)
}
func TestService_Get_BadStatus(t *testing.T) {
// Имитация ответа с ошибкой
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "bad request", http.StatusBadRequest)
}))
defer server.Close()
creds := NewCredintials("user", "pass")
svc := NewService(server.URL, 2, creds, &logging.TestLogger{})
ctx := context.Background()
info, err := svc.Get(ctx, "testuser")
assert.Error(t, err)
assert.Nil(t, info)
assert.Equal(t, ErrBadRequest, err)
}
func TestService_Get_InvalidJSON(t *testing.T) {
// Имитация некорректного JSON
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("invalid json"))
}))
defer server.Close()
creds := NewCredintials("user", "pass")
svc := NewService(server.URL, 2, creds, &logging.TestLogger{})
ctx := context.Background()
info, err := svc.Get(ctx, "testuser")
assert.Error(t, err)
assert.Nil(t, info)
}

View File

@@ -0,0 +1,62 @@
package service
import (
"catcher/app/internal/config"
"catcher/app/internal/lib/caching"
"catcher/app/internal/lib/logging"
"catcher/app/internal/models"
"catcher/app/internal/service/redirect"
"catcher/app/internal/service/replicate"
"catcher/app/internal/service/sentry/sending"
"context"
"github.com/google/uuid"
)
type ProjectyService struct {
ctx context.Context
config *config.Config
cacher caching.Cacher
logger logging.Logger
}
func NewProjectyService(appCtx models.AppContext) *ProjectyService {
return &ProjectyService{
ctx: appCtx.Ctx,
config: appCtx.Config,
cacher: appCtx.Cacher,
logger: appCtx.Logger}
}
func (p ProjectyService) SendEvent(projectId string, input models.Event) (*models.SendEventResult, error) {
// op := "ProjectyService.SendEvent"
prj, err := p.config.ProjectById(projectId)
if err != nil {
return nil, ErrBadProject
}
appCtx := models.NewAppContext(p.ctx, p.config, p.cacher, p.logger)
repl := replicate.New(appCtx)
var svc replicate.ConvertEventer = repl
event, err := svc.ConvertEvent(prj, input)
if err != nil {
return nil, ErrBadConvert
}
id := string(event.EventID)
if id == "" {
id = uuid.New().String()
}
svcEvent := sending.NewEvent(prj, id, event, appCtx)
eventID, err := redirect.Send(svcEvent, p.config.Registry.Timeout)
result := models.SendEventResult{
ID: id,
EventID: eventID,
}
return &result, err
}

View File

@@ -0,0 +1,85 @@
package redirect
import (
"catcher/app/internal/models"
"context"
"fmt"
"time"
"github.com/cockroachdb/errors"
)
var errTimeout = errors.New("timeout exceeded")
var errSending = errors.New("sending error")
type Sender interface {
Send() (*models.EventID, error)
}
type Report struct {
models.AppContext
ID string
Data []byte
}
func NewReport(id string, data []byte, appCtx models.AppContext) Report {
return Report{
ID: id,
Data: data,
AppContext: appCtx,
}
}
func Send(sender Sender, timeout int) (*models.EventID, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
type result struct {
eventID *models.EventID
err error
}
ch := make(chan result)
go func() {
defer func() {
if r := recover(); r != nil {
var err error
switch e := r.(type) {
case error:
err = e
case string:
err = errors.New(e)
default:
err = fmt.Errorf("panic: %v", e)
}
err = errors.WithStackDepth(err, 2)
ch <- result{nil, err}
}
}()
eventID, err := sender.Send()
ch <- result{eventID, err}
}()
var res result
select {
case res = <-ch:
switch {
case res.err != nil:
return nil, res.err
case res.eventID.IsEmpty():
return nil, errSending
}
case <-ctx.Done():
return nil, errTimeout
}
return res.eventID, nil
}

View File

@@ -0,0 +1,80 @@
package service
import (
"catcher/app/internal/config"
"catcher/app/internal/lib/caching"
"catcher/app/internal/lib/logging"
"catcher/app/internal/models"
"catcher/app/internal/service/redirect"
"catcher/app/internal/service/replicate"
"catcher/app/internal/service/sentry/reporting"
"context"
"errors"
)
var ErrBadRequest = errors.New("bad request")
var ErrBadProject = errors.New("no project setting")
var ErrBadConvert = errors.New("bad convert")
type ConvertReporter interface {
ConvertReport(r redirect.Report) (*models.RepportData, error)
}
type RegistryService struct {
ctx context.Context
config *config.Config
cacher caching.Cacher
logger logging.Logger
}
func NewRegistryService(appCtx models.AppContext) *RegistryService {
return &RegistryService{
ctx: appCtx.Ctx,
config: appCtx.Config,
cacher: appCtx.Cacher,
logger: appCtx.Logger}
}
func (r *RegistryService) GetInfo(input models.RegistryInput) models.RegistryInfo {
var needSendReport bool
if _, err := r.config.ProjectByName(input.ConfigName); err == nil {
needSendReport = true
} else {
needSendReport = false
}
return models.RegistryInfo{
NeedSendReport: needSendReport,
UserMessage: r.config.RegistryUserMessage(),
DumpType: r.config.RegistryDumpType(),
}
}
func (r *RegistryService) PushReport(input models.RegistryPushReportInput) (*models.RegistryPushReportResult, error) {
appCtx := models.NewAppContext(r.ctx, r.config, r.cacher, r.logger)
report := redirect.NewReport(input.ID, input.Data, appCtx)
repl := replicate.New(appCtx)
rData, err := ConvertReport(repl, report)
if err != nil {
return nil, ErrBadConvert
}
svcReport := reporting.NewReport(input.ID, rData.Prj, *rData.Data, rData.Files, appCtx)
eventID, err := redirect.Send(svcReport, r.config.Registry.Timeout)
result := models.RegistryPushReportResult{
ID: input.ID,
EventID: eventID,
}
return &result, err
}
func ConvertReport(cnv ConvertReporter, r redirect.Report) (*models.RepportData, error) {
return cnv.ConvertReport(r)
}

View File

@@ -0,0 +1,105 @@
package replicate
import (
"catcher/app/internal/config"
"catcher/app/internal/lib/gitbsl"
"catcher/app/internal/models"
"catcher/app/internal/sentryhub/normalize"
"catcher/app/internal/service/sentry/stacking"
"time"
"github.com/getsentry/sentry-go"
"github.com/jinzhu/copier"
)
type ConvertEventer interface {
ConvertEvent(prj config.Project, input models.Event) (*sentry.Event, error)
}
func (s Service) ConvertEvent(prj config.Project, input models.Event) (*sentry.Event, error) {
event := input.Event
if s.Config.UseDebug() {
event.Timestamp = time.Now()
} else {
event.Timestamp = time.Time(input.Timestamp)
}
if event.Platform == "" {
event.Platform = prj.Sentry.Platform
}
event.Exception = s.ConvertExeptions(prj, input.Exception)
event.Message = normalize.RemoveFromSecondBrace(event.Message, true)
event.Message = normalize.RemoveFromSecondBrace(event.Message, false) // два раза
return &event, nil
}
func (s Service) ConvertExeptions(prj config.Project, input models.Exception) []sentry.Exception {
exeptions := make([]sentry.Exception, len(input.Values))
for i, value := range input.Values {
exeption := sentry.Exception{}
exeption.Type = value.Type
exeption.Value = value.Value
stacktrace := sentry.Stacktrace{}
var exeptModule string
frames := make([]sentry.Frame, len(value.Stacktrace.Frames))
for j, valFrame := range value.Stacktrace.Frames {
frame := sentry.Frame{}
copier.Copy(&frame, &valFrame)
if frame.Platform == "" {
frame.Platform = prj.Sentry.Platform
}
module := valFrame.Module
if exeptModule == "" {
exeptModule = module
}
isExternal := gitbsl.IsExternalModule(module) || gitbsl.IsExpansion(module)
inApp := !isExternal
frame.InApp = inApp
s.absPath(&prj, &valFrame, &frame)
frames[j] = frame
}
stacktrace.Frames = frames
exeption.Module = exeptModule
exeption.Stacktrace = &stacktrace
svcStack := stacking.New(prj, s.AppContext)
svcStack.AddContextAround(exeption.Stacktrace)
exeptions[i] = exeption
}
return exeptions
}
func (s Service) absPath(prj *config.Project, v *models.Frame, frame *sentry.Frame) {
if !frame.InApp || frame.AbsPath != "" {
return
}
var m string
if v.ModuleAbs != "" {
m = v.ModuleAbs
} else {
m = v.Module
}
result, _ := gitbsl.NewPath(m, prj.Git.SourceCodeRoot, s.Logger).AbsPath()
frame.AbsPath = result
}

View File

@@ -0,0 +1,213 @@
package replicate
import (
"catcher/app/internal/lib/iofiles"
"catcher/app/internal/lib/logging"
"catcher/app/internal/models"
"catcher/app/internal/service/redirect"
"catcher/app/internal/service/project/userinfo"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
)
const (
reportJson = "report.json"
reportZip = "report.zip"
)
var errBadProject = errors.New("no project setting")
type Service struct {
models.AppContext
}
func New(appCtx models.AppContext) Service {
return Service{
AppContext: appCtx}
}
func (s Service) ConvertReport(r redirect.Report) (*models.RepportData, error) {
const op = "redirect.convert"
dir := filepath.Join(s.Config.WorkingDir, models.DirWeb, models.DirTemp)
fileName := fmt.Sprintf("%s_%s", r.ID, reportZip)
src := filepath.Join(dir, fileName)
if err := iofiles.SaveDataToFile(r.Data, src); err != nil {
s.Logger.Error("Не удалось выгрузить файл",
s.Logger.Op(op),
s.Logger.Str("name", src),
s.Logger.Err(err))
return nil, err
}
s.Logger.Debug("Сохранен файл",
s.Logger.Str("name", src))
dest := filepath.Join(dir, r.ID)
if err := iofiles.Unzip(src, dest); err != nil {
s.Logger.Error("Не удалось распаковать файл",
s.Logger.Op(op),
s.Logger.Str("name", src),
s.Logger.Err(err))
return nil, err
}
s.Logger.Debug("Распакован файл",
s.Logger.Str("name", src),
s.Logger.Str("dest", dest))
srcjson := filepath.Join(dest, reportJson)
repportData, err := fileToData(srcjson, s.Logger)
if err != nil {
return nil, err
}
s.Logger.Debug("Прочитан файл",
s.Logger.Str("name", srcjson))
files, err := listFiles(dest, s.Logger)
if err != nil {
return nil, err
}
prj, err := repportData.ProjectByConfig(s.Config)
if err != nil {
s.Logger.Error("Нет настройки для проекта",
s.Logger.Op(op),
s.Logger.Err(err))
return nil, errBadProject
}
rd := &models.RepportData{
ID: r.ID,
Prj: prj,
Data: repportData,
Files: files,
Src: src,
SrcDirFiles: dest,
}
defer s.CleanReport(rd)
svc := prj.Service
if svc.Use || rd.Data.SessionInfo.UserInfo.Empty() {
creds := userinfo.NewCredintials(svc.Credintials.UserName, svc.Credintials.Password)
svcUserInfo := userinfo.NewService(svc.Url, svc.IimeOut, creds, s.Logger)
s.AddUserInfo(svcUserInfo, prj, rd)
}
return rd, nil
}
func (s Service) CleanReport(rd *models.RepportData) error {
const op = "redirect.clean"
if !s.Config.DeleteTempFiles {
return nil
}
s.Logger.Debug("Очистка",
s.Logger.Str("src", rd.Src))
if err := os.Remove(rd.Src); err != nil {
s.Logger.Error("Ошибка удаления файла",
s.Logger.Op(op),
s.Logger.Str("Src", rd.Src),
s.Logger.Err(err))
return err
}
if err := os.RemoveAll(rd.SrcDirFiles); err != nil {
s.Logger.Error("Ошибка удаления каталога",
s.Logger.Op(op),
s.Logger.Str("SrcDirFiles", rd.SrcDirFiles),
s.Logger.Err(err))
return err
}
return nil
}
func fileToData(src string, logger logging.Logger) (*models.Repport, error) {
const op = "redirect.fileToData"
byteValue, err := iofiles.FileToByte(src)
if err != nil {
logger.Error("Не удалось прочитать файл",
logger.Op(op),
logger.Str("name", src),
logger.Err(err))
return nil, err
}
var repport models.Repport
err = json.Unmarshal(byteValue, &repport)
if err != nil {
logger.Error("Неверный формат файла",
logger.Op(op),
logger.Str("name", src),
logger.Err(err))
return nil, err
}
return &repport, nil
}
func listFiles(dest string, logger logging.Logger) ([]models.FileData, error) {
const op = "redirect.listFiles"
filesDir, err := os.ReadDir(dest)
if err != nil {
logger.Error("ReadDir",
logger.Op(op),
logger.Str("dest", dest),
logger.Err(err))
return nil, err
}
var files []models.FileData
for _, f := range filesDir {
if f.IsDir() {
continue
}
fileName := f.Name()
if fileName == reportJson {
continue
}
src := filepath.Join(dest, fileName)
byteValue, err := iofiles.FileToByte(src)
if err != nil {
logger.Error("Не удалось прочитать файл",
logger.Op(op),
logger.Str("name", src),
logger.Err(err))
return nil, err
}
files = append(files, models.FileData{
Name: fileName,
Data: byteValue,
})
logger.Debug("Получен файл",
logger.Str("name", src))
}
return files, nil
}

View File

@@ -0,0 +1,54 @@
package replicate
import (
"catcher/app/internal/config"
"catcher/app/internal/models"
"context"
"fmt"
"time"
"github.com/jinzhu/copier"
)
type Geter interface {
Get(ctx context.Context, input string) (*models.UserInfo, error)
}
func (s Service) AddUserInfo(g Geter, prj config.Project, rd *models.RepportData) {
const op = "userInfo.AddUserInfo"
var userName string
if s.Config.UseDebug() && prj.Service.Test.UserName != "" {
userName = prj.Service.Test.UserName
} else {
userName = rd.Data.SessionInfo.UserName
}
var res *models.UserInfo
key := fmt.Sprintf("%s:%s:%s", op, prj.Name, userName)
useCache := prj.Service.Cache.Use
if useCache {
if x, found := s.Cacher.Get(s.Ctx, key); found {
s.Logger.Debug("Используем кэш UserInfo",
s.Logger.Op(op),
s.Logger.Str("key", key))
res = x.(*models.UserInfo)
}
}
if res == nil {
var err error
res, err = g.Get(s.Ctx, userName)
if err != nil {
return
}
if useCache {
s.Cacher.Set(s.Ctx, key, res, time.Duration(prj.Service.Cache.Expiration)*time.Minute)
}
}
copier.Copy(&rd.Data.SessionInfo.UserInfo, &res)
}

View File

@@ -0,0 +1,195 @@
package eventing
import (
"catcher/app/internal/config"
"catcher/app/internal/models"
"catcher/app/internal/service/project/nonexept"
"context"
"fmt"
"strings"
"time"
"github.com/getsentry/sentry-go"
)
const (
opSending = "hub.CaptureEvent"
expirationCache = 5
)
type Service struct {
event *sentry.Event
models.AppContext
prj config.Project
key string
}
func New(e *sentry.Event, prj config.Project, appCtx models.AppContext) *Service {
s := &Service{
event: e,
prj: prj,
AppContext: appCtx,
}
s.generateKey()
return s
}
func (s *Service) generateKey() bool {
exception := s.event.Exception
if len(exception) == 0 {
return false
}
stacktrace := exception[0].Stacktrace
if stacktrace == nil {
return false
}
frames := stacktrace.Frames
if len(frames) == 0 {
return false
}
var key string
for _, f := range frames {
if f.StackStart {
key = fmt.Sprintf("%s:%d", f.Module, f.Lineno)
}
}
if key == "" {
return false
}
s.key = fmt.Sprintf("%s:%s:%s", s.prj.Name, s.event.User.Username, key)
return true
}
type nonExepter interface {
Get(ctx context.Context) ([]string, error)
}
func (s *Service) EventNeedSend() bool {
prj := s.prj
if !prj.Service.Exeptions.Use {
return true
}
var exepts []string
// Проверим на исключения
key := fmt.Sprintf("%s:exeptions", prj.Id)
x, found := s.Cacher.Get(s.Ctx, key)
if found {
exepts = x.([]string)
} else {
svc := prj.Service
creds := nonexept.NewCredintials(svc.Credintials.UserName, svc.Credintials.Password)
svcNonExept := nonexept.NewService(svc.Url, svc.IimeOut, creds, s.Logger)
var nonExepter nonExepter = svcNonExept
e, err := nonExepter.Get(s.Ctx)
if err != nil {
return true
}
s.Cacher.Set(s.Ctx, key, e, time.Duration(s.prj.Service.Exeptions.Cache.Expiration)*time.Minute)
exepts = e
}
t := s.typeExeption()
for _, e := range exepts {
if strings.Contains(t, e) {
s.Logger.Debug("Найдено значение в списке пропукаемых исключений",
s.Logger.Str("value", t),
s.Logger.Str("exept", e))
return false
}
}
return true
}
func (s *Service) IsEventSent(ctx context.Context) (*models.EventID, bool) {
const op = "eventing.IsEventSent"
key := s.keySending()
if !s.prj.Sentry.SendingCache.Use || key == "" {
return nil, false
}
opCtx := opKey(ctx, op)
var eventID *models.EventID
x, found := s.Cacher.Get(s.Ctx, key)
if found {
eventID = x.(*models.EventID)
s.Logger.Debug("Используем кэш Event. Cooбщение в Sentry уже было отправлено",
s.Logger.Op(opCtx),
s.Logger.Str("key", key),
s.Logger.Str("eventID", eventID.String()))
}
return eventID, found
}
func (s *Service) AddCacheSending(ctx context.Context, eventID *models.EventID) {
const op = "eventing.AddCacheSending"
key := s.keySending()
if !s.prj.Sentry.SendingCache.Use || key == "" {
return
}
opCtx := opKey(ctx, op)
s.Cacher.Set(s.Ctx, key, eventID, time.Duration(s.prj.Sentry.SendingCache.Expiration)*time.Minute)
s.Logger.Debug("Добавлен кэш сообщения",
s.Logger.Op(opCtx),
s.Logger.Str("key", key),
s.Logger.Str("eventID", eventID.String()))
}
func (s *Service) keySending() string {
return fmt.Sprintf("%s:%s", opSending, s.key)
}
func (s *Service) typeExeption() string {
var value string
for _, e := range s.event.Exception {
return e.Type
}
return value
}
func opKey(ctx context.Context, op string) string {
const opKey models.CtxKey = "op"
var res string
opCtx, ok := ctx.Value(opKey).(string)
if ok {
res = fmt.Sprintf("%s:%s", opCtx, op)
} else {
res = op
}
return res
}

View File

@@ -0,0 +1,244 @@
package reporting
import (
"catcher/app/internal/lib/ioimage"
"catcher/app/internal/sentryhub/normalize"
"fmt"
"log/slog"
"github.com/getsentry/sentry-go"
)
const (
eventLogger = "1с_service_logger"
emptyMessage = "Ошибка выполнения"
)
func (r Report) event() (*sentry.Event, error) {
data := r.Data
Id := normalize.Id(data.Id)
event := &sentry.Event{
Timestamp: data.Time,
EventID: sentry.EventID(Id),
Message: r.message(),
Level: sentry.LevelError,
Contexts: r.contexts(),
User: r.user(),
Breadcrumbs: r.breadcrumbs(),
Exception: r.exception(),
Modules: r.modules(),
Release: normalize.Release(data.AdditionalInfo, data.ConfigInfo.Version),
Platform: r.Prj.Sentry.Platform,
Dist: r.dist(),
Environment: r.Prj.Sentry.Environment,
Attachments: r.attachments(),
Tags: r.tags(),
Logger: eventLogger,
}
if len(event.Exception) == 0 && event.Message == "" {
event.Message = emptyMessage
}
return event, nil
}
func (r Report) message() string {
return ""
}
func (r Report) dist() string {
return r.Prj.Id
}
// Contexts Interface
// https://develop.sentry.dev/sdk/data-model/event-payloads/contexts/
func (r Report) contexts() map[string]sentry.Context {
ctx := make(map[string]sentry.Context)
clientInfo := r.Data.ClientInfo
if clientInfo.AppName != "" {
ctx["app"] = sentry.Context{"app_build": clientInfo.PlatformType,
"app_version": clientInfo.AppVersion,
"app_name": clientInfo.AppName}
}
systemInfo := clientInfo.SystemInfo
if systemInfo.OsVersion != "" {
ctx["os"] = sentry.Context{"name": systemInfo.OsVersion}
}
sessionInfo := r.Data.SessionInfo.UserInfo.SessionInfo
deviceName := sessionInfo.Device
if deviceName == "" {
deviceName = "PC"
}
if systemInfo.ClientID != "" {
ctx["device"] = sentry.Context{
"name": deviceName,
"memory_size": systemInfo.FullRAM,
"free_memory": systemInfo.FreeRAM,
"cpu_description": systemInfo.Processor,
"model_id": systemInfo.ClientID}
}
sessionData := sentry.Context{}
connection := sessionInfo.Connection
if connection != 0 {
sessionData["Connection"] = connection
}
session := sessionInfo.Session
if session != 0 {
sessionData["Session"] = session
}
if len(sessionData) != 0 {
ctx["Session Data"] = sessionData
}
return ctx
}
func (r Report) user() sentry.User {
sessionInfo := r.Data.SessionInfo
return sentry.User{
ID: sessionInfo.UserInfo.Id,
Username: sessionInfo.UserName,
IPAddress: normalize.Ip(sessionInfo.UserInfo.SessionInfo.IP),
}
}
func (r Report) breadcrumbs() []*sentry.Breadcrumb {
brc := make([]*sentry.Breadcrumb, 0)
brc = r.breadcrumbsErrorInfo(brc)
return brc
}
func (r Report) breadcrumbsErrorInfo(brc []*sentry.Breadcrumb) []*sentry.Breadcrumb {
// описание ошибки на втором уровне
// забираем все строки, кроме первой, т.к. первая уже будет в message и сюда заполнится автоматом
errorInfo := r.Data.ErrorInfo.ApplicationErrorInfo.Errors
for i := len(errorInfo) - 1; i >= 0; i-- {
errorData := errorInfo[i] //[2]any
message := normalize.Message(fmt.Sprintf("%v", errorData[0]))
if message == "" {
continue
}
сategory := "message"
brc = append(brc, &sentry.Breadcrumb{
Type: "info",
Category: сategory,
Message: message,
Level: sentry.LevelError,
Timestamp: r.Data.Time,
})
}
return brc
}
func (r Report) modules() map[string]string {
const emptyVersion string = "0.0.1"
result := make(map[string]string)
// Заполняем включенные расширения, кроме постоянных
extentions := r.Data.ConfigInfo.Extentions
for _, val := range extentions {
name := val[0]
if name == "" || r.Prj.ExistExtention(name) {
continue
}
extention := normalize.SplitString(name, "(", ")")
version := extention[1]
if version == "" {
version = emptyVersion
}
result[extention[0]] = version
}
return result
}
func (r Report) attachments() []*sentry.Attachment {
cAttachments := r.Prj.Sentry.Attachments
if !cAttachments.Use {
return nil
}
filesData := r.Files
if len(filesData) == 0 {
return nil
}
var result []*sentry.Attachment
const full = 100
var percent = full
if cAttachments.Сompress.Use || cAttachments.Сompress.Percent < percent {
percent = cAttachments.Сompress.Percent
}
for _, v := range filesData {
var data = v.Data
if percent < full {
if cdata, err := ioimage.Compress(data, uint(percent)); err == nil {
r.Logger.Debug("Сжато вложение",
r.Logger.Str("Name", v.Name),
slog.Int("Percent", percent))
data = cdata
}
}
result = append(result, &sentry.Attachment{
Filename: v.Name,
Payload: data,
})
}
return result
}
func (r Report) tags() map[string]string {
res := make(map[string]string)
userInfo := r.Data.SessionInfo.UserInfo
if userInfo.City != "" {
res["place.city"] = userInfo.City
}
if userInfo.Branch != "" {
res["place.branch"] = userInfo.Branch
}
if userInfo.Position != "" {
res["user.position"] = userInfo.Position
}
if !userInfo.Started.IsZero() {
res["user.started"] = userInfo.Started.Format("02-01-2006T15:04:05")
}
return res
}

View File

@@ -0,0 +1,167 @@
package reporting
import (
"catcher/app/internal/lib/gitbsl"
"catcher/app/internal/sentryhub/normalize"
"catcher/app/internal/service/sentry/stacking"
"fmt"
"strings"
"github.com/getsentry/sentry-go"
)
type Gitter interface {
GetFileContent(filePath string) (*string, error)
}
func (r Report) exception() []sentry.Exception {
message := r.exceptionMessage()
if len(message) == 0 {
return nil
}
result := make([]sentry.Exception, 1)
// Разделим и получим Type и Value
// "{ОбщийМодуль.ОбменСSentry.Модуль(1114)}: Ошибка при вызове метода контекста (Вставить)",
part := normalize.SplitString(message[0], "{", "}")
t := part[0]
v := part[1]
t = strings.TrimPrefix(t, ":")
t = strings.TrimSpace(t)
exeption := sentry.Exception{
Type: t,
Value: v,
}
// Подставим Module без номера строки
//ОбщийМодуль.ОбменСSentry.Модуль(1114)
part = normalize.SplitString(v, "(", ")")
m := part[0]
if m != "" {
exeption.Module = m
}
exeption.Stacktrace = r.stacktrace()
result[0] = exeption
return result
}
// frame
// "ВнешняяОбработка.Ошибка.Форма.Форма.Форма", 9, "СоСтекомНаСервере()"
func (r Report) stacktrace() *sentry.Stacktrace {
stack := r.Data.ErrorInfo.ApplicationErrorInfo.Stack
lenstack := len(stack)
if lenstack == 0 {
return nil
}
frames := make([]sentry.Frame, lenstack)
for i, v := range stack {
moduleAbs := fmt.Sprintf("%v", v[0])
module := normalize.RemoveModuleNameSuffix(moduleAbs)
function := fmt.Sprintf("%v", v[2])
function = normalize.RemoveModuleNameSuffix(function)
isExternal := gitbsl.IsExternalModule(module) || gitbsl.IsExpansion(module)
inApp := !isExternal
fileName := fileNameByModule(module, isExternal)
contextLine := contextByFunction(function)
var lineno int
l, ok := v[1].(float64)
if ok {
lineno = int(l)
}
absPath := r.absPath(moduleAbs, isExternal)
var stackStart bool
if len(frames)-1 == i {
stackStart = true
}
frames[i] = sentry.Frame{
Function: function,
Module: module,
Lineno: lineno,
InApp: inApp,
Filename: fileName,
ContextLine: contextLine,
StackStart: stackStart,
AbsPath: absPath,
Platform: r.Prj.Sentry.Platform,
}
}
stacktrace := &sentry.Stacktrace{
Frames: frames,
}
svcStack := stacking.New(r.Prj, r.AppContext)
svcStack.AddContextAround(stacktrace)
return stacktrace
}
func (r Report) exceptionMessage() []string {
var result []string
errorInfo := r.Data.ErrorInfo.ApplicationErrorInfo.Errors
// текст ошибки на втором уровне
for _, v := range errorInfo {
if len(v) > 0 {
result = append(result, fmt.Sprintf("%v", v[0]))
}
}
return result
}
func (r Report) absPath(m string, isExternal bool) string {
if isExternal {
return ""
}
result, _ := gitbsl.NewPath(m, r.Prj.Git.SourceCodeRoot, r.Logger).AbsPath()
return result
}
func fileNameByModule(m string, isExternal bool) string {
if isExternal {
part := strings.Split(m, ".")
if len(part) > 1 {
return fmt.Sprintf("%v.%v", part[0], part[1])
} else {
return m
}
} else {
return m
}
}
func contextByFunction(f string) string {
return strings.TrimSpace(f)
// part := strings.Split(f, ".")
// return part[len(part)-1]
}

View File

@@ -0,0 +1,116 @@
package reporting
import (
"catcher/app/internal/config"
"catcher/app/internal/models"
"catcher/app/internal/sentryhub"
"catcher/app/internal/service/sentry/eventing"
"context"
"log/slog"
"github.com/cockroachdb/errors"
)
type Report struct {
models.AppContext
ID string
Prj config.Project
Data models.Repport
Files []models.FileData
}
func NewReport(id string, prj config.Project, data models.Repport, files []models.FileData, appCtx models.AppContext) Report {
return Report{
ID: id,
Prj: prj,
Data: data,
Files: files,
AppContext: appCtx,
}
}
type eventer interface {
IsEventSent(ctx context.Context) (*models.EventID, bool)
EventNeedSend() bool
}
func (r Report) Send() (*models.EventID, error) {
const op = "reporting.send"
const opKey models.CtxKey = "op"
ctx := context.WithValue(r.Ctx, opKey, op)
data := r.Data
var prj config.Project
var err error
additionalInfo := data.AdditionalInfo
if additionalInfo != "" {
prj, err = r.Config.ProjectById(additionalInfo)
} else {
prj, err = r.Config.ProjectByName(data.ConfigInfo.Name)
}
if err != nil {
r.Logger.Error("Ошибка получение настроек проекта",
r.Logger.Op(op),
r.Logger.Err(err))
return nil, err
}
event, err := r.event()
if err != nil {
r.Logger.Error("Ошибка сборки event",
r.Logger.Op(op),
r.Logger.Str("ID", r.ID),
r.Logger.Err(err))
return nil, err
}
// cache
svcEventing := eventing.New(event, prj, r.AppContext)
var eventer eventer = svcEventing
x, found := eventer.IsEventSent(ctx)
if found {
return x, nil
}
// nonexept
if !eventer.EventNeedSend() {
r.Logger.Debug("Сообщение пропущено, не требует отправки",
r.Logger.Str("ID", r.ID))
res := models.EventID(r.ID) // возвращаем входящий ID
return &res, nil
}
hub, err := sentryhub.Get(prj, r.AppContext)
if err != nil {
r.Logger.Error("Ошибка получение настроек проекта",
r.Logger.Op(op),
r.Logger.Err(err))
return nil, err
}
hub.Scope().ClearBreadcrumbs()
eventID := hub.CaptureEvent(event)
if eventID == nil {
r.Logger.Error("Ошибка отправки соообщения в Sentry",
r.Logger.Op(op),
r.Logger.Str("ID", r.ID))
return nil, errors.Errorf("sending error %s", r.ID)
}
r.Logger.Debug("Отправлено в Sentry сообщение",
r.Logger.Str("ID", r.ID),
slog.Any("eventID", eventID),
r.Logger.Op(op))
res := models.EventID(*eventID)
svcEventing.AddCacheSending(ctx, &res)
return &res, nil
}

View File

@@ -0,0 +1,80 @@
package sending
import (
"catcher/app/internal/config"
"catcher/app/internal/models"
"catcher/app/internal/sentryhub"
"catcher/app/internal/service/sentry/eventing"
"context"
"log/slog"
"github.com/cockroachdb/errors"
"github.com/getsentry/sentry-go"
)
type Event struct {
models.AppContext
Prj config.Project
Event *sentry.Event
ID string
}
func NewEvent(prj config.Project, Id string, e *sentry.Event, appCtx models.AppContext) Event {
return Event{
Prj: prj,
Event: e,
ID: Id,
AppContext: appCtx,
}
}
type eventer interface {
IsEventSent(ctx context.Context) (*models.EventID, bool)
}
func (e Event) Send() (*models.EventID, error) {
const op = "sending.event.send"
const opKey models.CtxKey = "op"
ctx := context.WithValue(e.Ctx, opKey, op)
// cache
svcEventing := eventing.New(e.Event, e.Prj, e.AppContext)
var eventer eventer = svcEventing
x, found := eventer.IsEventSent(ctx)
if found {
return x, nil
}
var err error
hub, err := sentryhub.Get(e.Prj, e.AppContext)
if err != nil {
e.Logger.Error("Ошибка получение настроек проекта",
e.Logger.Op(op),
e.Logger.Err(err))
return nil, err
}
hub.Scope().ClearBreadcrumbs()
eventID := hub.CaptureEvent(e.Event)
if eventID == nil {
e.Logger.Error("Ошибка отправки соообщения в Sentry",
e.Logger.Op(op),
e.Logger.Str("ID", e.ID))
return nil, errors.Errorf("sending error %s", e.ID)
}
e.Logger.Debug("Отправлено в Sentry сообщение",
e.Logger.Str("ID", e.ID),
slog.Any("eventID", eventID),
e.Logger.Op(op))
res := models.EventID(*eventID)
svcEventing.AddCacheSending(ctx, &res)
return &res, nil
}

View File

@@ -0,0 +1,51 @@
package stacking
import (
"catcher/app/internal/config"
"catcher/app/internal/git"
"catcher/app/internal/models"
"github.com/getsentry/sentry-go"
)
type Service struct {
models.AppContext
prj config.Project
}
func New(prj config.Project, appCtx models.AppContext) Service {
return Service{
prj: prj,
AppContext: appCtx,
}
}
func (s Service) AddContextAround(stacktrace *sentry.Stacktrace) error {
const op = "stack.AddContextAround"
gitter, err := s.prj.GetGit()
if err != nil {
s.Logger.Error("Не удалось создать объект git",
s.Logger.Err(err),
s.Logger.Op(op))
return err
}
if gitter == nil {
return nil // выключено
}
svcGit := git.New(gitter, s.prj, s.AppContext)
for i, frame := range stacktrace.Frames {
ctxAround := svcGit.GetContextAround(frame.AbsPath, frame.Lineno)
stacktrace.Frames[i].PreContext = ctxAround.Pre
stacktrace.Frames[i].PostContext = ctxAround.Post
}
return nil
}

View File

@@ -0,0 +1,34 @@
package service
import (
"catcher/app/internal/lib/caching"
"catcher/app/internal/lib/logging"
"catcher/app/internal/models"
"context"
)
type ServiceService struct {
ctx context.Context
cacher caching.Cacher
logger logging.Logger
}
func NewServiceService(appCtx models.AppContext) *ServiceService {
return &ServiceService{
ctx: appCtx.Ctx,
cacher: appCtx.Cacher,
logger: appCtx.Logger}
}
func (s ServiceService) ClearCache() error {
const op = "service.ClearCache"
err := s.cacher.Clear(s.ctx)
if err != nil {
return err
}
s.logger.Debug("Кэш очищен", s.logger.Op(op))
return nil
}

View File

@@ -0,0 +1,34 @@
package service
import (
"catcher/app/internal/models"
)
type Registry interface {
GetInfo(input models.RegistryInput) models.RegistryInfo
PushReport(input models.RegistryPushReportInput) (*models.RegistryPushReportResult, error)
}
type Projecty interface {
SendEvent(projectId string, input models.Event) (*models.SendEventResult, error)
}
type Service interface {
ClearCache() error
}
type Services struct {
Registry
Projecty
Service
}
func NewService(appCtx models.AppContext) *Services {
return &Services{
Registry: NewRegistryService(appCtx),
Projecty: NewProjectyService(appCtx),
Service: NewServiceService(appCtx),
}
}

View File

@@ -0,0 +1,45 @@
package logging
import "log/slog"
// TestLogger реализует интерфейс logging.Logger для тестов
type TestLogger struct {
debugMessages []string
warnMessages []string
infoMessages []string
errorMessages []string
}
func (l *TestLogger) Debug(msg string, attrs ...slog.Attr) {
l.debugMessages = append(l.debugMessages, msg)
}
func (l *TestLogger) Info(msg string, attrs ...slog.Attr) {
l.infoMessages = append(l.infoMessages, msg)
}
func (l *TestLogger) Warn(msg string, attrs ...slog.Attr) {
l.warnMessages = append(l.warnMessages, msg)
}
func (l *TestLogger) Error(msg string, attrs ...slog.Attr) {
l.errorMessages = append(l.errorMessages, msg)
}
func (l *TestLogger) Err(err error) slog.Attr {
return slog.Any("error", err)
}
func (l *TestLogger) Op(value string) slog.Attr {
return slog.Attr{
Key: "op",
Value: slog.StringValue(value),
}
}
func (l *TestLogger) Str(key, value string) slog.Attr {
return slog.Attr{
Key: key,
Value: slog.StringValue(value),
}
}

69
config/config_debug.yml Normal file
View File

@@ -0,0 +1,69 @@
Server:
Port: "8000"
Registry:
UserMessage: "Разработчики получат информацию об ошибке автоматически." # Текст, который будет показан пользователю в качестве дополнительной информации об ошибке
DumpType: 1 # Тип дампа, который нужно приложить к отчету об ошибке (аналогичен значению атрибута type элемента dump файла logcfg.xml)
Timeout: 20
Projects:
- Name: "1C"
ID: "1C"
Service:
Use: false
Url: ""
IimeOut: 20 #Минут
Credintials:
UserName: ""
Password: ""
Cache:
Use: true
Expiration: 60 #Минут
Exeptions:
Use: true
Cache:
Use: true
Expiration: 1440 #24 часа
Test:
UserName: "user"
Sentry:
Dsn: ""
Environment: "dev"
Platform: "Other"
ContextAround:
Use: true
Quantity: 6
Cache:
Use: true
Expiration: 60
Attachments:
Use: false
Сompress:
Use: true
Percent: 30
SendingCache:
Use: true
Expiration: 3
Git:
Use: true
Url: ""
Path: ""
Token: ""
Branch: "master"
SourceCodeRoot: "config"
Extentions:
- "VAExtension (1.05)"
Log:
Debug: true
Level: 5 # Уровень логирования от 2 до 5, где 2 - ошибка, 3 - предупреждение, 4 - информация, 5 - дебаг
OutputInFile: false # включить логирование в каталог LogDir
Dir: "logs" # каталог логов
DeleteTempFiles: false # Удалать файлы из web/temp
Sentry:
Use: false # Включить sentry для отлова внутренних ошибок
Dsn: ""
AttachStacktrace: true
TracesSampleRate: 1.0
EnableTracing: true

1103
docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

1079
docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

762
docs/swagger.yaml Normal file
View File

@@ -0,0 +1,762 @@
basePath: /api
definitions:
app_internal_handler.errorResponse:
properties:
message:
type: string
type: object
catcher_app_internal_models.Event:
properties:
breadcrumbs:
items:
$ref: '#/definitions/sentry.Breadcrumb'
type: array
check_in:
$ref: '#/definitions/sentry.CheckIn'
contexts:
additionalProperties:
$ref: '#/definitions/sentry.Context'
type: object
debug_meta:
$ref: '#/definitions/sentry.DebugMeta'
dist:
type: string
environment:
type: string
event_id:
type: string
exception:
$ref: '#/definitions/catcher_app_internal_models.Exception'
extra:
additionalProperties: true
type: object
fingerprint:
items:
type: string
type: array
level:
$ref: '#/definitions/sentry.Level'
logger:
type: string
message:
type: string
modules:
additionalProperties:
type: string
type: object
monitor_config:
$ref: '#/definitions/sentry.MonitorConfig'
platform:
type: string
release:
type: string
request: {}
sdk:
$ref: '#/definitions/sentry.SdkInfo'
server_name:
type: string
spans:
items:
$ref: '#/definitions/sentry.Span'
type: array
start_timestamp:
type: string
tags:
additionalProperties:
type: string
type: object
threads:
items:
$ref: '#/definitions/sentry.Thread'
type: array
timestamp:
type: string
transaction:
type: string
transaction_info:
$ref: '#/definitions/sentry.TransactionInfo'
type:
type: string
user:
$ref: '#/definitions/sentry.User'
type: object
catcher_app_internal_models.Exception:
properties:
values:
items:
$ref: '#/definitions/catcher_app_internal_models.ExceptionValue'
type: array
type: object
catcher_app_internal_models.ExceptionValue:
properties:
stacktrace:
$ref: '#/definitions/catcher_app_internal_models.Stacktrace'
type:
type: string
value:
type: string
type: object
catcher_app_internal_models.Frame:
properties:
abs_path:
type: string
context_line:
type: string
filename:
type: string
function:
type: string
in_app:
type: boolean
lineno:
type: integer
module:
type: string
module_abs:
type: string
stack_start:
type: boolean
type: object
catcher_app_internal_models.RegistryInfo:
properties:
dumpType:
type: integer
needSendReport:
type: boolean
userMessage:
type: string
type: object
catcher_app_internal_models.RegistryInput:
properties:
ErrorCategories:
items:
type: string
type: array
appName:
type: string
appStackHash:
type: string
appVersion:
type: string
clientID:
type: string
clientStackHash:
type: string
configHash:
type: string
configName:
type: string
configVersion:
type: string
configurationInterfaceLanguageCode:
type: string
platformInterfaceLanguageCode:
type: string
platformType:
type: string
reportID:
type: string
required:
- configName
type: object
catcher_app_internal_models.RegistryPushReportResult:
properties:
eventID:
type: string
id:
type: string
type: object
catcher_app_internal_models.SendEventResult:
properties:
eventID:
type: string
id:
type: string
type: object
catcher_app_internal_models.Stacktrace:
properties:
frames:
items:
$ref: '#/definitions/catcher_app_internal_models.Frame'
type: array
type: object
sentry.Breadcrumb:
properties:
category:
type: string
data:
additionalProperties: true
type: object
level:
$ref: '#/definitions/sentry.Level'
message:
type: string
timestamp:
type: string
type:
type: string
type: object
sentry.CheckIn:
properties:
check_in_id:
description: Check-In ID (unique and client generated)
type: string
duration:
allOf:
- $ref: '#/definitions/time.Duration'
description: The duration of the check-in. Will only take effect if the status
is ok or error.
monitor_slug:
description: The distinct slug of the monitor.
type: string
status:
allOf:
- $ref: '#/definitions/sentry.CheckInStatus'
description: The status of the check-in.
type: object
sentry.CheckInStatus:
enum:
- in_progress
- ok
- error
type: string
x-enum-varnames:
- CheckInStatusInProgress
- CheckInStatusOK
- CheckInStatusError
sentry.Context:
additionalProperties: true
type: object
sentry.DebugMeta:
properties:
images:
items:
$ref: '#/definitions/sentry.DebugMetaImage'
type: array
sdk_info:
$ref: '#/definitions/sentry.DebugMetaSdkInfo'
type: object
sentry.DebugMetaImage:
properties:
arch:
description: macho,elf,pe
type: string
code_file:
description: macho,elf,pe,wasm,sourcemap
type: string
code_id:
description: macho,elf,pe,wasm
type: string
debug_file:
description: macho,elf,pe,wasm
type: string
debug_id:
description: macho,elf,pe,wasm,sourcemap
type: string
image_addr:
description: macho,elf,pe
type: string
image_size:
description: macho,elf,pe
type: integer
image_vmaddr:
description: macho,elf,pe
type: string
type:
description: all
type: string
uuid:
description: proguard
type: string
type: object
sentry.DebugMetaSdkInfo:
properties:
sdk_name:
type: string
version_major:
type: integer
version_minor:
type: integer
version_patchlevel:
type: integer
type: object
sentry.Exception:
properties:
mechanism:
$ref: '#/definitions/sentry.Mechanism'
module:
type: string
stacktrace:
$ref: '#/definitions/sentry.Stacktrace'
thread_id:
type: integer
type:
description: used as the main issue title
type: string
value:
description: used as the main issue subtitle
type: string
type: object
sentry.Frame:
properties:
abs_path:
type: string
addr_mode:
type: string
colno:
type: integer
context_line:
type: string
filename:
type: string
function:
type: string
image_addr:
type: string
in_app:
type: boolean
instruction_addr:
type: string
lineno:
type: integer
module:
description: |-
Module is, despite the name, the Sentry protocol equivalent of a Go
package's import path.
type: string
package:
description: |-
Package and the below are not used for Go stack trace frames. In
other platforms it refers to a container where the Module can be
found. For example, a Java JAR, a .NET Assembly, or a native
dynamic library. They exists for completeness, allowing the
construction and reporting of custom event payloads.
type: string
platform:
type: string
post_context:
items:
type: string
type: array
pre_context:
items:
type: string
type: array
stack_start:
type: boolean
symbol:
type: string
symbol_addr:
type: string
vars:
additionalProperties: true
type: object
type: object
sentry.Level:
enum:
- debug
- info
- warning
- error
- fatal
type: string
x-enum-varnames:
- LevelDebug
- LevelInfo
- LevelWarning
- LevelError
- LevelFatal
sentry.Mechanism:
properties:
data:
additionalProperties: {}
type: object
description:
type: string
exception_id:
type: integer
handled:
type: boolean
help_link:
type: string
is_exception_group:
type: boolean
parent_id:
type: integer
source:
type: string
type:
type: string
type: object
sentry.MonitorConfig:
properties:
checkin_margin:
description: |-
The allowed margin of minutes after the expected check-in time that
the monitor will not be considered missed for.
type: integer
failure_issue_threshold:
description: The number of consecutive failed check-ins it takes before an
issue is created.
type: integer
max_runtime:
description: |-
The allowed duration in minutes that the monitor may be `in_progress`
for before being considered failed due to timeout.
type: integer
recovery_threshold:
description: The number of consecutive OK check-ins it takes before an issue
is resolved.
type: integer
schedule: {}
timezone:
description: |-
A tz database string representing the timezone which the monitor's execution schedule is in.
See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
type: string
type: object
sentry.Request:
properties:
cookies:
type: string
data:
type: string
env:
additionalProperties:
type: string
type: object
headers:
additionalProperties:
type: string
type: object
method:
type: string
query_string:
type: string
url:
type: string
type: object
sentry.SdkInfo:
properties:
integrations:
items:
type: string
type: array
name:
type: string
packages:
items:
$ref: '#/definitions/sentry.SdkPackage'
type: array
version:
type: string
type: object
sentry.SdkPackage:
properties:
name:
type: string
version:
type: string
type: object
sentry.Span:
properties:
data:
additionalProperties: true
type: object
description:
type: string
name:
type: string
op:
type: string
origin:
type: string
parent_span_id:
items:
type: integer
type: array
span_id:
items:
type: integer
type: array
start_timestamp:
type: string
status:
$ref: '#/definitions/sentry.SpanStatus'
tags:
additionalProperties:
type: string
type: object
timestamp:
type: string
trace_id:
items:
type: integer
type: array
type: object
sentry.SpanStatus:
enum:
- 0
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
type: integer
x-enum-varnames:
- SpanStatusUndefined
- SpanStatusOK
- SpanStatusCanceled
- SpanStatusUnknown
- SpanStatusInvalidArgument
- SpanStatusDeadlineExceeded
- SpanStatusNotFound
- SpanStatusAlreadyExists
- SpanStatusPermissionDenied
- SpanStatusResourceExhausted
- SpanStatusFailedPrecondition
- SpanStatusAborted
- SpanStatusOutOfRange
- SpanStatusUnimplemented
- SpanStatusInternalError
- SpanStatusUnavailable
- SpanStatusDataLoss
- SpanStatusUnauthenticated
- maxSpanStatus
sentry.Stacktrace:
properties:
frames:
items:
$ref: '#/definitions/sentry.Frame'
type: array
frames_omitted:
items:
type: integer
type: array
type: object
sentry.Thread:
properties:
crashed:
type: boolean
current:
type: boolean
id:
type: string
name:
type: string
stacktrace:
$ref: '#/definitions/sentry.Stacktrace'
type: object
sentry.TransactionInfo:
properties:
source:
$ref: '#/definitions/sentry.TransactionSource'
type: object
sentry.TransactionSource:
enum:
- custom
- url
- route
- view
- component
- task
type: string
x-enum-varnames:
- SourceCustom
- SourceURL
- SourceRoute
- SourceView
- SourceComponent
- SourceTask
sentry.User:
properties:
data:
additionalProperties:
type: string
type: object
email:
type: string
id:
type: string
ip_address:
type: string
name:
type: string
username:
type: string
type: object
time.Duration:
enum:
- -9223372036854775808
- 9223372036854775807
- 1
- 1000
- 1000000
- 1000000000
- 60000000000
- 3600000000000
- -9223372036854775808
- 9223372036854775807
- 1
- 1000
- 1000000
- 1000000000
- 60000000000
- 3600000000000
type: integer
x-enum-varnames:
- minDuration
- maxDuration
- Nanosecond
- Microsecond
- Millisecond
- Second
- Minute
- Hour
- minDuration
- maxDuration
- Nanosecond
- Microsecond
- Millisecond
- Second
- Minute
- Hour
host: localhost:8000
info:
contact: {}
description: Catcher API Service
title: Catcher
version: "1.0"
paths:
/api/prj/:id/sendEvent:
post:
consumes:
- application/json
description: Отправка Event в Sentry
operationId: sendEvent
parameters:
- description: Данные события
in: body
name: input
required: true
schema:
$ref: '#/definitions/catcher_app_internal_models.Event'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/catcher_app_internal_models.SendEventResult'
default:
description: ""
schema:
$ref: '#/definitions/app_internal_handler.errorResponse'
summary: Send Event
tags:
- Event
/api/reg:
get:
description: Проверка работы метода getInfo
operationId: getInfo
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/catcher_app_internal_models.RegistryInfo'
default:
description: ""
schema:
$ref: '#/definitions/app_internal_handler.errorResponse'
summary: Get Info
tags:
- info
/api/reg/getInfo:
post:
consumes:
- application/json
description: Получение информации для отчета об ошибки
operationId: getInfoPost
parameters:
- description: Значения для отчета об ошибке
in: body
name: input
required: true
schema:
$ref: '#/definitions/catcher_app_internal_models.RegistryInput'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/catcher_app_internal_models.RegistryInfo'
default:
description: ""
schema:
$ref: '#/definitions/app_internal_handler.errorResponse'
summary: Get Info Post
tags:
- Info
/api/reg/pushReport:
post:
consumes:
- multipart/form-data
description: Отправка отчета об ошибки
operationId: pushReport
parameters:
- description: Файл в архиве формата https://its.1c.ru/db/v8327doc#bookmark:dev:TI000002558
in: formData
name: file
required: true
type: file
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/catcher_app_internal_models.RegistryPushReportResult'
default:
description: ""
schema:
$ref: '#/definitions/app_internal_handler.errorResponse'
summary: Push Report
tags:
- Report
/api/service/clearCache:
get:
description: Очищает все данные, сохранённые в кэше
operationId: clearCache
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties:
type: string
type: object
default:
description: ""
schema:
$ref: '#/definitions/app_internal_handler.errorResponse'
summary: Clear cache
tags:
- Cache
swagger: "2.0"

75
go.mod Normal file
View File

@@ -0,0 +1,75 @@
module catcher
go 1.24.1
require (
github.com/cockroachdb/errors v1.12.0
github.com/creasty/defaults v1.8.0
github.com/getsentry/sentry-go v0.32.0
github.com/getsentry/sentry-go/gin v0.32.0
github.com/getsentry/sentry-go/slog v0.32.0
github.com/gin-gonic/gin v1.10.0
github.com/go-playground/validator/v10 v10.26.0
github.com/google/uuid v1.6.0
github.com/jinzhu/copier v0.4.0
github.com/joho/godotenv v1.5.1
github.com/kardianos/service v1.2.2
github.com/lmittmann/tint v1.0.7
github.com/mattn/go-colorable v0.1.14
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/stretchr/testify v1.10.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.4
gitlab.com/gitlab-org/api/client-go v0.129.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.16.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.32.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)

227
go.sum Normal file
View File

@@ -0,0 +1,227 @@
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo=
github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk=
github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY=
github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY=
github.com/getsentry/sentry-go/gin v0.32.0 h1:9wDh8sLkRNhjBra99kVFPdsTHs/9ADqMMHaW9huyIVc=
github.com/getsentry/sentry-go/gin v0.32.0/go.mod h1:4iRO82BCDcQzMCBq/phv4Px22p6pvNod3OqGLTopQe4=
github.com/getsentry/sentry-go/slog v0.32.0 h1:cXGYJzRI5aVh3Ku3KBpehtfGDjrX5ZPZSqPNTh/Su/o=
github.com/getsentry/sentry-go/slog v0.32.0/go.mod h1:CzWubrnXm36RwJXRm9Bou2pBvxNYP1XfJgIdEtX4uMM=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
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/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60=
github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y=
github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
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/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
gitlab.com/gitlab-org/api/client-go v0.129.0 h1:o9KLn6fezmxBQWYnQrnilwyuOjlx4206KP0bUn3HuBE=
gitlab.com/gitlab-org/api/client-go v0.129.0/go.mod h1:ZhSxLAWadqP6J9lMh40IAZOlOxBLPRh7yFOXR/bMJWM=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
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.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.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/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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
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=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@@ -0,0 +1,14 @@
@echo off
rem run this script as admin
if not exist catcher.exe (
echo "file not found"
goto :exit
)
sc create Catcher binpath= "%CD%\catcher.exe -config=config/config.yml" start= auto DisplayName= "Catcher"
net start Catcher
sc query Catcher
:exit

View File

@@ -0,0 +1,5 @@
@echo off
rem run this script as admin
net stop Catcher
sc delete Catcher