mirror of
				https://github.com/vsilchenkov/catcher.git
				synced 2025-10-30 23:43:51 +02:00 
			
		
		
		
	init
This commit is contained in:
		
							
								
								
									
										22
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal 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
									
								
							
							
						
						
									
										28
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
										Normal 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
									
								
							
							
						
						
									
										7
									
								
								Makefile
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										12
									
								
								README.md
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										53
									
								
								app/build/build.go
									
									
									
									
									
										Normal 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) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										15
									
								
								app/cmd/catcher/catcher.exe.manifest
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/cmd/catcher/catcher.exe.manifest
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										
											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
									
								
							
							
						
						
									
										95
									
								
								app/cmd/catcher/main.go
									
									
									
									
									
										Normal 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)) | ||||
| 	} | ||||
|  | ||||
| } | ||||
							
								
								
									
										40
									
								
								app/cmd/catcher/versioninfo.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								app/cmd/catcher/versioninfo.json
									
									
									
									
									
										Normal 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" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										168
									
								
								app/internal/config/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								app/internal/config/config.go
									
									
									
									
									
										Normal 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)) | ||||
| } | ||||
							
								
								
									
										91
									
								
								app/internal/config/project.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								app/internal/config/project.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										121
									
								
								app/internal/git/git.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										75
									
								
								app/internal/git/gitlab/gitlab.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								app/internal/git/gitlab/gitlab.go
									
									
									
									
									
										Normal 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 | ||||
|  | ||||
| } | ||||
							
								
								
									
										113
									
								
								app/internal/handler/handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								app/internal/handler/handler.go
									
									
									
									
									
										Normal 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, | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										40
									
								
								app/internal/handler/projecty.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								app/internal/handler/projecty.go
									
									
									
									
									
										Normal 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}) | ||||
|  | ||||
| } | ||||
							
								
								
									
										90
									
								
								app/internal/handler/registry.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								app/internal/handler/registry.go
									
									
									
									
									
										Normal 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) | ||||
|  | ||||
| } | ||||
							
								
								
									
										20
									
								
								app/internal/handler/response.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/internal/handler/response.go
									
									
									
									
									
										Normal 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()}) | ||||
| } | ||||
							
								
								
									
										35
									
								
								app/internal/handler/service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/internal/handler/service.go
									
									
									
									
									
										Normal 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", | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										22
									
								
								app/internal/lib/caching/caching.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/internal/lib/caching/caching.go
									
									
									
									
									
										Normal 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, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										30
									
								
								app/internal/lib/caching/memory/memory.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/internal/lib/caching/memory/memory.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										263
									
								
								app/internal/lib/gitbsl/gitbsl.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								app/internal/lib/gitbsl/gitbsl.go
									
									
									
									
									
										Normal 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"}, | ||||
| 	} | ||||
|  | ||||
| } | ||||
							
								
								
									
										105
									
								
								app/internal/lib/gitbsl/gitbsl_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								app/internal/lib/gitbsl/gitbsl_test.go
									
									
									
									
									
										Normal 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) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										79
									
								
								app/internal/lib/iofiles/iofiles.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								app/internal/lib/iofiles/iofiles.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										49
									
								
								app/internal/lib/ioimage/ioimage.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								app/internal/lib/ioimage/ioimage.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										52
									
								
								app/internal/lib/logging/hmulti.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								app/internal/lib/logging/hmulti.go
									
									
									
									
									
										Normal 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} | ||||
| } | ||||
							
								
								
									
										160
									
								
								app/internal/lib/logging/logger.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								app/internal/lib/logging/logger.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										42
									
								
								app/internal/lib/logging/sentry.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								app/internal/lib/logging/sentry.go
									
									
									
									
									
										Normal 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, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										49
									
								
								app/internal/lib/times/times.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								app/internal/lib/times/times.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										55
									
								
								app/internal/models/event.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								app/internal/models/event.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
							
								
								
									
										30
									
								
								app/internal/models/models.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/internal/models/models.go
									
									
									
									
									
										Normal 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} | ||||
| } | ||||
							
								
								
									
										38
									
								
								app/internal/models/registry.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								app/internal/models/registry.go
									
									
									
									
									
										Normal 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", | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										132
									
								
								app/internal/models/repport.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								app/internal/models/repport.go
									
									
									
									
									
										Normal 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{} | ||||
| } | ||||
							
								
								
									
										138
									
								
								app/internal/sentryhub/normalize/normalize.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								app/internal/sentryhub/normalize/normalize.go
									
									
									
									
									
										Normal 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 | ||||
|  | ||||
| } | ||||
							
								
								
									
										120
									
								
								app/internal/sentryhub/normalize/normalize_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								app/internal/sentryhub/normalize/normalize_test.go
									
									
									
									
									
										Normal 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, "Если пустой строки нет после второй '{', и запятая отсутствует, просто добавляем '}'") | ||||
| } | ||||
							
								
								
									
										86
									
								
								app/internal/sentryhub/sentryhub.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								app/internal/sentryhub/sentryhub.go
									
									
									
									
									
										Normal 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) | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										72
									
								
								app/internal/server/http/http.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								app/internal/server/http/http.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										100
									
								
								app/internal/server/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								app/internal/server/server.go
									
									
									
									
									
										Normal 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 | ||||
|  | ||||
| } | ||||
							
								
								
									
										121
									
								
								app/internal/service/project/nonexept/nonexept.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								app/internal/service/project/nonexept/nonexept.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										96
									
								
								app/internal/service/project/nonexept/nonexept_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								app/internal/service/project/nonexept/nonexept_test.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
							
								
								
									
										154
									
								
								app/internal/service/project/userinfo/userinfo.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								app/internal/service/project/userinfo/userinfo.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										92
									
								
								app/internal/service/project/userinfo/userinfo_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								app/internal/service/project/userinfo/userinfo_test.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
							
								
								
									
										62
									
								
								app/internal/service/projecty.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								app/internal/service/projecty.go
									
									
									
									
									
										Normal 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 | ||||
|  | ||||
| } | ||||
							
								
								
									
										85
									
								
								app/internal/service/redirect/redirect.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								app/internal/service/redirect/redirect.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										80
									
								
								app/internal/service/registry.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								app/internal/service/registry.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
							
								
								
									
										105
									
								
								app/internal/service/replicate/event.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								app/internal/service/replicate/event.go
									
									
									
									
									
										Normal 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 | ||||
|  | ||||
| } | ||||
							
								
								
									
										213
									
								
								app/internal/service/replicate/report.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										213
									
								
								app/internal/service/replicate/report.go
									
									
									
									
									
										Normal 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 | ||||
|  | ||||
| } | ||||
							
								
								
									
										54
									
								
								app/internal/service/replicate/userinfo.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								app/internal/service/replicate/userinfo.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
							
								
								
									
										195
									
								
								app/internal/service/sentry/eventing/eventing.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								app/internal/service/sentry/eventing/eventing.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										244
									
								
								app/internal/service/sentry/reporting/event.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										244
									
								
								app/internal/service/sentry/reporting/event.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										167
									
								
								app/internal/service/sentry/reporting/exception.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								app/internal/service/sentry/reporting/exception.go
									
									
									
									
									
										Normal 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] | ||||
| } | ||||
							
								
								
									
										116
									
								
								app/internal/service/sentry/reporting/reporting.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								app/internal/service/sentry/reporting/reporting.go
									
									
									
									
									
										Normal 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 | ||||
|  | ||||
| } | ||||
							
								
								
									
										80
									
								
								app/internal/service/sentry/sending/event.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								app/internal/service/sentry/sending/event.go
									
									
									
									
									
										Normal 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 | ||||
|  | ||||
| } | ||||
							
								
								
									
										51
									
								
								app/internal/service/sentry/stacking/stacking.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								app/internal/service/sentry/stacking/stacking.go
									
									
									
									
									
										Normal 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 | ||||
|  | ||||
| } | ||||
							
								
								
									
										34
									
								
								app/internal/service/service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								app/internal/service/service.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										34
									
								
								app/internal/service/services.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								app/internal/service/services.go
									
									
									
									
									
										Normal 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), | ||||
| 	} | ||||
| } | ||||
|  | ||||
|  | ||||
							
								
								
									
										45
									
								
								app/internal/testutil/logging/logging.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/internal/testutil/logging/logging.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										69
									
								
								config/config_debug.yml
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										1103
									
								
								docs/docs.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1079
									
								
								docs/swagger.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1079
									
								
								docs/swagger.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										762
									
								
								docs/swagger.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										762
									
								
								docs/swagger.yaml
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										75
									
								
								go.mod
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										227
									
								
								go.sum
									
									
									
									
									
										Normal 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= | ||||
							
								
								
									
										14
									
								
								service/windows/install.bat
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								service/windows/install.bat
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										5
									
								
								service/windows/uninstall.bat
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								service/windows/uninstall.bat
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| @echo off | ||||
| rem run this script as admin | ||||
|  | ||||
| net stop Catcher | ||||
| sc delete Catcher | ||||
		Reference in New Issue
	
	Block a user