log-proxy/main.go
2026-01-23 10:03:52 +07:00

290 lines
9.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 <target_url> 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())
}