package main import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "net/http/httputil" "net/url" "os" "strings" "time" ) type LoggingProxy struct { target *url.URL proxy *httputil.ReverseProxy } type RequestLog struct { Timestamp time.Time `json:"timestamp"` Method string `json:"method"` URL string `json:"url"` FullURL string `json:"full_url"` TargetURL string `json:"target_url"` Headers map[string][]string `json:"headers"` Body string `json:"body"` RemoteAddr string `json:"remote_addr"` } type ResponseLog struct { Timestamp time.Time `json:"timestamp"` StatusCode int `json:"status_code"` Headers map[string][]string `json:"headers"` Body string `json:"body"` Duration string `json:"duration"` } type LogEntry struct { Request RequestLog `json:"request"` Response ResponseLog `json:"response"` } func NewLoggingProxy(targetURL string) (*LoggingProxy, error) { target, err := url.Parse(targetURL) if err != nil { return nil, fmt.Errorf("invalid target URL: %w", err) } proxy := httputil.NewSingleHostReverseProxy(target) originalDirector := proxy.Director proxy.Director = func(req *http.Request) { originalDirector(req) // Устанавливаем правильный хост req.Host = target.Host // Убеждаемся, что схема правильная req.URL.Scheme = target.Scheme req.URL.Host = target.Host // Сохраняем оригинальный путь и query параметры // (они уже должны быть сохранены через originalDirector) } return &LoggingProxy{ target: target, proxy: proxy, }, nil } func (lp *LoggingProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { start := time.Now() // Логируем запрос перед отправкой reqLog := lp.logRequest(r) // Создаем recorder для перехвата ответа recorder := &responseRecorder{ ResponseWriter: w, statusCode: http.StatusOK, body: &bytes.Buffer{}, headers: make(http.Header), } // Настраиваем ModifyResponse для перехвата метаданных ответа originalModifyResponse := lp.proxy.ModifyResponse lp.proxy.ModifyResponse = func(resp *http.Response) error { if originalModifyResponse != nil { if err := originalModifyResponse(resp); err != nil { return err } } // Сохраняем статус код из ответа для логирования recorder.statusCode = resp.StatusCode recorder.statusSetFromResp = true // Сохраняем заголовки для логирования // ReverseProxy автоматически скопирует заголовки в ResponseWriter if recorder.headers == nil { recorder.headers = make(http.Header) } for k, v := range resp.Header { recorder.headers[k] = make([]string, len(v)) copy(recorder.headers[k], v) } // Важно: не трогаем resp.Body, чтобы ReverseProxy мог правильно передать его клиенту return nil } // Выполняем проксирование запроса lp.proxy.ServeHTTP(recorder, r) // Логируем ответ после получения duration := time.Since(start) respLog := ResponseLog{ Timestamp: time.Now(), StatusCode: recorder.statusCode, Headers: recorder.headers, Body: recorder.body.String(), Duration: duration.String(), } logEntry := LogEntry{ Request: reqLog, Response: respLog, } lp.printLog(logEntry) } func (lp *LoggingProxy) logRequest(r *http.Request) RequestLog { // Читаем тело запроса bodyBytes, _ := io.ReadAll(r.Body) r.Body.Close() // Восстанавливаем тело для проксирования r.Body = io.NopCloser(bytes.NewReader(bodyBytes)) // Форматируем тело для логирования bodyStr := string(bodyBytes) if len(bodyStr) > 0 { var jsonData interface{} if err := json.Unmarshal(bodyBytes, &jsonData); err == nil { if formatted, err := json.MarshalIndent(jsonData, "", " "); err == nil { bodyStr = string(formatted) } } } // Формируем полный URL запроса fullURL := r.URL.String() if r.URL.Scheme == "" { scheme := "http" if r.TLS != nil { scheme = "https" } fullURL = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.URL.String()) } // Формируем целевой URL targetURL := fmt.Sprintf("%s://%s%s", lp.target.Scheme, lp.target.Host, r.URL.Path) if r.URL.RawQuery != "" { targetURL += "?" + r.URL.RawQuery } return RequestLog{ Timestamp: time.Now(), Method: r.Method, URL: r.URL.String(), FullURL: fullURL, TargetURL: targetURL, Headers: r.Header, Body: bodyStr, RemoteAddr: r.RemoteAddr, } } func (lp *LoggingProxy) printLog(entry LogEntry) { fmt.Println(strings.Repeat("=", 80)) fmt.Printf("[%s] %s %s\n", entry.Request.Timestamp.Format("2006-01-02 15:04:05"), entry.Request.Method, entry.Request.URL) fmt.Printf("From URL: %s\n", entry.Request.FullURL) fmt.Printf("To URL: %s\n", entry.Request.TargetURL) fmt.Printf("Remote: %s\n", entry.Request.RemoteAddr) if len(entry.Request.Headers) > 0 { fmt.Println("\nRequest Headers:") for k, v := range entry.Request.Headers { fmt.Printf(" %s: %s\n", k, strings.Join(v, ", ")) } } if entry.Request.Body != "" { fmt.Println("\nRequest Body:") fmt.Println(entry.Request.Body) } fmt.Printf("\n[%s] Response: %d (%s)\n", entry.Response.Timestamp.Format("2006-01-02 15:04:05"), entry.Response.StatusCode, entry.Response.Duration) if len(entry.Response.Headers) > 0 { fmt.Println("\nResponse Headers:") for k, v := range entry.Response.Headers { fmt.Printf(" %s: %s\n", k, strings.Join(v, ", ")) } } if entry.Response.Body != "" { fmt.Println("\nResponse Body:") fmt.Println(entry.Response.Body) } fmt.Println(strings.Repeat("=", 80)) fmt.Println() if jsonData, err := json.MarshalIndent(entry, "", " "); err == nil { fmt.Println("JSON Log:") fmt.Println(string(jsonData)) fmt.Println() } } type responseRecorder struct { http.ResponseWriter statusCode int body *bytes.Buffer headers http.Header wroteHeader bool statusSetFromResp bool } func (rr *responseRecorder) Header() http.Header { return rr.ResponseWriter.Header() } func (rr *responseRecorder) WriteHeader(code int) { if !rr.wroteHeader { // Используем статус код из ModifyResponse, если он был установлен, иначе используем переданный if !rr.statusSetFromResp { rr.statusCode = code } rr.wroteHeader = true // Сохраняем все заголовки перед отправкой (для логирования) if rr.headers == nil { rr.headers = make(http.Header) } // Обновляем заголовки из ResponseWriter (они могут быть изменены ReverseProxy) for k, v := range rr.ResponseWriter.Header() { rr.headers[k] = make([]string, len(v)) copy(rr.headers[k], v) } // Вызываем WriteHeader оригинального ResponseWriter с правильным статус кодом rr.ResponseWriter.WriteHeader(rr.statusCode) } } func (rr *responseRecorder) Write(b []byte) (int, error) { // Если WriteHeader еще не вызван, вызываем его со статусом 200 if !rr.wroteHeader { rr.WriteHeader(http.StatusOK) } // Сохраняем тело для логирования if rr.body == nil { rr.body = &bytes.Buffer{} } rr.body.Write(b) // Важно: записываем в оригинальный ResponseWriter, чтобы ответ вернулся клиенту return rr.ResponseWriter.Write(b) } func main() { targetURL := os.Getenv("TARGET_URL") if targetURL == "" { if len(os.Args) < 2 { log.Fatal("Usage: log-proxy or set TARGET_URL environment variable") } targetURL = os.Args[1] } port := os.Getenv("PORT") if port == "" { port = "8020" } proxy, err := NewLoggingProxy(targetURL) if err != nil { log.Fatalf("Failed to create proxy: %v", err) } fmt.Printf("Starting log proxy server on port %s\n", port) fmt.Printf("Proxying requests to: %s\n", targetURL) fmt.Println("Press Ctrl+C to stop") fmt.Println() server := &http.Server{ Addr: ":" + port, Handler: proxy, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, } log.Fatal(server.ListenAndServe()) }