mirror of
https://github.com/dalbodeule/hop-gate.git
synced 2025-12-07 20:35:44 +09:00
[feat](observability): add Prometheus metrics and /metrics endpoint
- Introduced Prometheus metrics tracking for DTLS handshakes, HTTP requests, and proxy errors. - Defined counters and histograms with labels for detailed observability. - Registered metrics via `MustRegister` during server initialization. - Added `/metrics` endpoint protected by host domain filtering. - Updated HTTP handler to capture request metadata and record metrics. - Integrated metrics tracking for DTLS handshake processes and various error conditions. - Updated `go.mod` and `go.sum` with Prometheus client dependencies.
This commit is contained in:
@@ -9,14 +9,18 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
|
||||||
"github.com/dalbodeule/hop-gate/internal/acme"
|
"github.com/dalbodeule/hop-gate/internal/acme"
|
||||||
"github.com/dalbodeule/hop-gate/internal/config"
|
"github.com/dalbodeule/hop-gate/internal/config"
|
||||||
"github.com/dalbodeule/hop-gate/internal/dtls"
|
"github.com/dalbodeule/hop-gate/internal/dtls"
|
||||||
"github.com/dalbodeule/hop-gate/internal/logging"
|
"github.com/dalbodeule/hop-gate/internal/logging"
|
||||||
|
"github.com/dalbodeule/hop-gate/internal/observability"
|
||||||
"github.com/dalbodeule/hop-gate/internal/protocol"
|
"github.com/dalbodeule/hop-gate/internal/protocol"
|
||||||
"github.com/dalbodeule/hop-gate/internal/store"
|
"github.com/dalbodeule/hop-gate/internal/store"
|
||||||
)
|
)
|
||||||
@@ -126,6 +130,45 @@ var (
|
|||||||
sessionsByDomain = make(map[string]*dtlsSessionWrapper)
|
sessionsByDomain = make(map[string]*dtlsSessionWrapper)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// statusRecorder 는 HTTP 응답 상태 코드를 캡처하기 위한 래퍼입니다.
|
||||||
|
// Prometheus 메트릭에서 status 라벨을 기록하는 데 사용합니다.
|
||||||
|
type statusRecorder struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *statusRecorder) WriteHeader(code int) {
|
||||||
|
w.status = code
|
||||||
|
w.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hostDomainHandler 는 HOP_SERVER_DOMAIN 에 지정된 도메인으로만 요청을 허용하는 래퍼입니다.
|
||||||
|
// Host 헤더에서 포트를 제거한 뒤 소문자 비교를 수행합니다.
|
||||||
|
func hostDomainHandler(allowedDomain string, logger logging.Logger, next http.Handler) http.Handler {
|
||||||
|
allowed := strings.ToLower(strings.TrimSpace(allowedDomain))
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if allowed != "" {
|
||||||
|
host := r.Host
|
||||||
|
if i := strings.Index(host, ":"); i != -1 {
|
||||||
|
host = host[:i]
|
||||||
|
}
|
||||||
|
host = strings.ToLower(strings.TrimSpace(host))
|
||||||
|
if host != allowed {
|
||||||
|
logger.Warn("rejecting request due to mismatched host", logging.Fields{
|
||||||
|
"allowed_domain": allowed,
|
||||||
|
"request_host": host,
|
||||||
|
"path": r.URL.Path,
|
||||||
|
})
|
||||||
|
// 메트릭/관리용 엔드포인트에 대해 호스트가 다르면 404 로 응답하여 노출을 최소화합니다.
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func registerSessionForDomain(domain string, sess dtls.Session, logger logging.Logger) {
|
func registerSessionForDomain(domain string, sess dtls.Session, logger logging.Logger) {
|
||||||
d := strings.ToLower(strings.TrimSpace(domain))
|
d := strings.ToLower(strings.TrimSpace(domain))
|
||||||
if d == "" {
|
if d == "" {
|
||||||
@@ -163,20 +206,37 @@ func newHTTPHandler(logger logging.Logger) http.Handler {
|
|||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
method := r.Method
|
||||||
|
|
||||||
|
// 상태 코드 캡처를 위한 래퍼
|
||||||
|
sr := &statusRecorder{
|
||||||
|
ResponseWriter: w,
|
||||||
|
status: http.StatusOK,
|
||||||
|
}
|
||||||
|
|
||||||
log := logger.With(logging.Fields{
|
log := logger.With(logging.Fields{
|
||||||
"component": "http_entry",
|
"component": "http_entry",
|
||||||
"method": r.Method,
|
"method": method,
|
||||||
"url": r.URL.String(),
|
"url": r.URL.String(),
|
||||||
"host": r.Host,
|
"host": r.Host,
|
||||||
})
|
})
|
||||||
log.Info("incoming http request", nil)
|
log.Info("incoming http request", nil)
|
||||||
|
|
||||||
|
// 요청 단위 Prometheus 메트릭 기록
|
||||||
|
defer func() {
|
||||||
|
elapsed := time.Since(start).Seconds()
|
||||||
|
statusCode := sr.status
|
||||||
|
observability.HTTPRequestsTotal.WithLabelValues(method, strconv.Itoa(statusCode)).Inc()
|
||||||
|
observability.HTTPRequestDurationSeconds.WithLabelValues(method).Observe(elapsed)
|
||||||
|
}()
|
||||||
|
|
||||||
// 1. ACME HTTP-01 webroot handling
|
// 1. ACME HTTP-01 webroot handling
|
||||||
// /.well-known/acme-challenge/{token} 는 HOP_ACME_WEBROOT 디렉터리에서 정적 파일로 서빙합니다.
|
// /.well-known/acme-challenge/{token} 는 HOP_ACME_WEBROOT 디렉터리에서 정적 파일로 서빙합니다.
|
||||||
if webroot != "" && strings.HasPrefix(r.URL.Path, "/.well-known/acme-challenge/") {
|
if webroot != "" && strings.HasPrefix(r.URL.Path, "/.well-known/acme-challenge/") {
|
||||||
token := strings.Trim(r.URL.Path, "/")
|
token := strings.Trim(r.URL.Path, "/")
|
||||||
if token == "" {
|
if token == "" {
|
||||||
http.Error(w, "invalid acme challenge path", http.StatusBadRequest)
|
observability.ProxyErrorsTotal.WithLabelValues("acme_http01_error").Inc()
|
||||||
|
http.Error(sr, "invalid acme challenge path", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
filePath := filepath.Join(webroot, token)
|
filePath := filepath.Join(webroot, token)
|
||||||
@@ -195,17 +255,19 @@ func newHTTPHandler(logger logging.Logger) http.Handler {
|
|||||||
log.Error("failed to open acme challenge file", logging.Fields{
|
log.Error("failed to open acme challenge file", logging.Fields{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
http.NotFound(w, r)
|
observability.ProxyErrorsTotal.WithLabelValues("acme_http01_error").Inc()
|
||||||
|
http.NotFound(sr, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
// ACME challenge 응답은 일반적으로 text/plain.
|
// ACME challenge 응답은 일반적으로 text/plain.
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
sr.Header().Set("Content-Type", "text/plain")
|
||||||
if _, err := io.Copy(w, f); err != nil {
|
if _, err := io.Copy(sr, f); err != nil {
|
||||||
log.Error("failed to write acme challenge response", logging.Fields{
|
log.Error("failed to write acme challenge response", logging.Fields{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
observability.ProxyErrorsTotal.WithLabelValues("acme_http01_error").Inc()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -219,7 +281,8 @@ func newHTTPHandler(logger logging.Logger) http.Handler {
|
|||||||
log.Warn("no dtls session for host", logging.Fields{
|
log.Warn("no dtls session for host", logging.Fields{
|
||||||
"host": r.Host,
|
"host": r.Host,
|
||||||
})
|
})
|
||||||
http.Error(w, "no backend client available", http.StatusBadGateway)
|
observability.ProxyErrorsTotal.WithLabelValues("no_dtls_session").Inc()
|
||||||
|
http.Error(sr, "no backend client available", http.StatusBadGateway)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,22 +295,23 @@ func newHTTPHandler(logger logging.Logger) http.Handler {
|
|||||||
log.Error("forward over dtls failed", logging.Fields{
|
log.Error("forward over dtls failed", logging.Fields{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
http.Error(w, "dtls forward failed", http.StatusBadGateway)
|
observability.ProxyErrorsTotal.WithLabelValues("dtls_forward_failed").Inc()
|
||||||
|
http.Error(sr, "dtls forward failed", http.StatusBadGateway)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 응답 헤더/바디 복원
|
// 응답 헤더/바디 복원
|
||||||
for k, vs := range protoResp.Header {
|
for k, vs := range protoResp.Header {
|
||||||
for _, v := range vs {
|
for _, v := range vs {
|
||||||
w.Header().Add(k, v)
|
sr.Header().Add(k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if protoResp.Status == 0 {
|
if protoResp.Status == 0 {
|
||||||
protoResp.Status = http.StatusOK
|
protoResp.Status = http.StatusOK
|
||||||
}
|
}
|
||||||
w.WriteHeader(protoResp.Status)
|
sr.WriteHeader(protoResp.Status)
|
||||||
if len(protoResp.Body) > 0 {
|
if len(protoResp.Body) > 0 {
|
||||||
if _, err := w.Write(protoResp.Body); err != nil {
|
if _, err := sr.Write(protoResp.Body); err != nil {
|
||||||
log.Warn("failed to write http response body", logging.Fields{
|
log.Warn("failed to write http response body", logging.Fields{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
@@ -265,6 +329,9 @@ func newHTTPHandler(logger logging.Logger) http.Handler {
|
|||||||
func main() {
|
func main() {
|
||||||
logger := logging.NewStdJSONLogger("server")
|
logger := logging.NewStdJSONLogger("server")
|
||||||
|
|
||||||
|
// Prometheus 메트릭 등록
|
||||||
|
observability.MustRegister()
|
||||||
|
|
||||||
// 1. 서버 설정 로드 (.env + 환경변수)
|
// 1. 서버 설정 로드 (.env + 환경변수)
|
||||||
cfg, err := config.LoadServerConfigFromEnv()
|
cfg, err := config.LoadServerConfigFromEnv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -396,10 +463,19 @@ func main() {
|
|||||||
// 5. HTTP / HTTPS 서버 시작
|
// 5. HTTP / HTTPS 서버 시작
|
||||||
httpHandler := newHTTPHandler(logger)
|
httpHandler := newHTTPHandler(logger)
|
||||||
|
|
||||||
|
// Prometheus /metrics 엔드포인트 및 메인 핸들러를 위한 mux 구성
|
||||||
|
httpMux := http.NewServeMux()
|
||||||
|
allowedDomain := strings.ToLower(strings.TrimSpace(cfg.Domain))
|
||||||
|
|
||||||
|
// /metrics 는 HOP_SERVER_DOMAIN 에 지정된 도메인으로만 접근 가능하도록 제한합니다.
|
||||||
|
// Admin Plane HTTP mux 도 이후 hostDomainHandler 를 통해 동일하게 제한해야 합니다.
|
||||||
|
httpMux.Handle("/metrics", hostDomainHandler(allowedDomain, logger, promhttp.Handler()))
|
||||||
|
httpMux.Handle("/", httpHandler)
|
||||||
|
|
||||||
// HTTP: 평문 포트
|
// HTTP: 평문 포트
|
||||||
httpSrv := &http.Server{
|
httpSrv := &http.Server{
|
||||||
Addr: cfg.HTTPListen,
|
Addr: cfg.HTTPListen,
|
||||||
Handler: httpHandler,
|
Handler: httpMux,
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
logger.Info("http server listening", logging.Fields{
|
logger.Info("http server listening", logging.Fields{
|
||||||
@@ -415,7 +491,7 @@ func main() {
|
|||||||
// HTTPS: ACME 기반 TLS 사용 (debug 모드에서도 ACME tls config 사용 가능)
|
// HTTPS: ACME 기반 TLS 사용 (debug 모드에서도 ACME tls config 사용 가능)
|
||||||
httpsSrv := &http.Server{
|
httpsSrv := &http.Server{
|
||||||
Addr: cfg.HTTPSListen,
|
Addr: cfg.HTTPSListen,
|
||||||
Handler: httpHandler,
|
Handler: httpMux,
|
||||||
TLSConfig: acmeTLSCfg,
|
TLSConfig: acmeTLSCfg,
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
@@ -435,7 +511,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DTLS 핸드셰이크 단계에서 HOP_SERVER_DOMAIN 으로 설정된 도메인만 허용하도록 래핑합니다.
|
// DTLS 핸드셰이크 단계에서 HOP_SERVER_DOMAIN 으로 설정된 도메인만 허용하도록 래핑합니다.
|
||||||
allowedDomain := strings.ToLower(strings.TrimSpace(cfg.Domain))
|
allowedDomain = strings.ToLower(strings.TrimSpace(cfg.Domain))
|
||||||
var validator dtls.DomainValidator = &domainGateValidator{
|
var validator dtls.DomainValidator = &domainGateValidator{
|
||||||
allowed: allowedDomain,
|
allowed: allowedDomain,
|
||||||
inner: baseValidator,
|
inner: baseValidator,
|
||||||
@@ -458,6 +534,9 @@ func main() {
|
|||||||
// 세션 종료/타임아웃 관리는 별도의 세션 매니저(TODO)에서 담당해야 합니다.
|
// 세션 종료/타임아웃 관리는 별도의 세션 매니저(TODO)에서 담당해야 합니다.
|
||||||
hsRes, err := dtls.PerformServerHandshake(ctx, s, validator, logger)
|
hsRes, err := dtls.PerformServerHandshake(ctx, s, validator, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// 핸드셰이크 실패 메트릭 기록
|
||||||
|
observability.DTLSHandshakesTotal.WithLabelValues("failure").Inc()
|
||||||
|
|
||||||
// PerformServerHandshake 내부에서 이미 상세 로그를 남기므로 여기서는 요약만 기록합니다.
|
// PerformServerHandshake 내부에서 이미 상세 로그를 남기므로 여기서는 요약만 기록합니다.
|
||||||
logger.Warn("dtls handshake failed", logging.Fields{
|
logger.Warn("dtls handshake failed", logging.Fields{
|
||||||
"session_id": s.ID(),
|
"session_id": s.ID(),
|
||||||
@@ -469,6 +548,9 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handshake 성공 메트릭 기록
|
||||||
|
observability.DTLSHandshakesTotal.WithLabelValues("success").Inc()
|
||||||
|
|
||||||
// Handshake 성공: 서버 측은 어떤 도메인이 연결되었는지 알 수 있습니다.
|
// Handshake 성공: 서버 측은 어떤 도메인이 연결되었는지 알 수 있습니다.
|
||||||
logger.Info("dtls handshake completed", logging.Fields{
|
logger.Info("dtls handshake completed", logging.Fields{
|
||||||
"session_id": s.ID(),
|
"session_id": s.ID(),
|
||||||
|
|||||||
9
go.mod
9
go.mod
@@ -8,6 +8,7 @@ require (
|
|||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/pion/dtls/v3 v3.0.7
|
github.com/pion/dtls/v3 v3.0.7
|
||||||
|
github.com/prometheus/client_golang v1.19.0
|
||||||
golang.org/x/net v0.47.0
|
golang.org/x/net v0.47.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,8 +16,10 @@ require (
|
|||||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 // indirect
|
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 // indirect
|
||||||
github.com/agext/levenshtein v1.2.3 // indirect
|
github.com/agext/levenshtein v1.2.3 // indirect
|
||||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bmatcuk/doublestar v1.3.4 // indirect
|
github.com/bmatcuk/doublestar v1.3.4 // indirect
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
github.com/go-openapi/inflect v0.19.0 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
@@ -25,13 +28,17 @@ require (
|
|||||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||||
github.com/pion/logging v0.2.4 // indirect
|
github.com/pion/logging v0.2.4 // indirect
|
||||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
|
github.com/prometheus/common v0.48.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.12.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/zclconf/go-cty v1.14.4 // indirect
|
github.com/zclconf/go-cty v1.14.4 // indirect
|
||||||
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
|
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
|
||||||
golang.org/x/crypto v0.44.0 // indirect
|
golang.org/x/crypto v0.45.0 // indirect
|
||||||
golang.org/x/mod v0.29.0 // indirect
|
golang.org/x/mod v0.29.0 // indirect
|
||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
golang.org/x/tools v0.38.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
20
go.sum
20
go.sum
@@ -8,10 +8,14 @@ github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7l
|
|||||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
|
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
|
||||||
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/go-acme/lego/v4 v4.28.1 h1:zt301JYF51UIEkpSXsdeGq9hRePeFzQCq070OdAmP0Q=
|
github.com/go-acme/lego/v4 v4.28.1 h1:zt301JYF51UIEkpSXsdeGq9hRePeFzQCq070OdAmP0Q=
|
||||||
@@ -28,8 +32,8 @@ 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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo=
|
github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo=
|
||||||
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
|
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
@@ -50,6 +54,14 @@ github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1
|
|||||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||||
|
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||||
|
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||||
|
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||||
|
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||||
|
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||||
|
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||||
|
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||||
@@ -62,6 +74,8 @@ github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWB
|
|||||||
github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
|
github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
|
||||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||||
|
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||||
|
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
@@ -74,5 +88,7 @@ golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
|||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||||
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
58
internal/observability/metrics.go
Normal file
58
internal/observability/metrics.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package observability
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 전역 레지스트리에 등록할 HopGate 메트릭들을 정의합니다.
|
||||||
|
// Prometheus 기본 네임스페이스를 사용하며, 메트릭 이름에 hopgate_ 접두어를 붙입니다.
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DTLS 핸드셰이크 총 횟수 (성공/실패 라벨 포함).
|
||||||
|
DTLSHandshakesTotal = prometheus.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "hopgate_dtls_handshakes_total",
|
||||||
|
Help: "Total number of DTLS handshakes, labeled by result.",
|
||||||
|
},
|
||||||
|
[]string{"result"}, // success, failure
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTP/Proxy 엔드포인트를 통해 들어온 요청 수 (메서드/상태 코드 라벨 포함).
|
||||||
|
HTTPRequestsTotal = prometheus.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "hopgate_http_requests_total",
|
||||||
|
Help: "Total number of HTTP requests handled by the proxy entrypoint, labeled by method and status code.",
|
||||||
|
},
|
||||||
|
[]string{"method", "status"},
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTP 요청 처리 시간 분포 (메서드 라벨 포함).
|
||||||
|
HTTPRequestDurationSeconds = prometheus.NewHistogramVec(
|
||||||
|
prometheus.HistogramOpts{
|
||||||
|
Name: "hopgate_http_request_duration_seconds",
|
||||||
|
Help: "Histogram of HTTP request latencies in seconds at the proxy entrypoint, labeled by method.",
|
||||||
|
Buckets: prometheus.DefBuckets,
|
||||||
|
},
|
||||||
|
[]string{"method"},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Proxy 에러 카운터 (에러 유형 라벨 포함).
|
||||||
|
ProxyErrorsTotal = prometheus.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "hopgate_proxy_errors_total",
|
||||||
|
Help: "Total number of proxy-related errors, labeled by error type.",
|
||||||
|
},
|
||||||
|
[]string{"type"}, // e.g. no_dtls_session, dtls_forward_failed, acme_http01_error
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// MustRegister 는 위에서 정의한 메트릭들을 전역 Prometheus 레지스트리에 등록합니다.
|
||||||
|
// 서버 시작 시 한 번만 호출해야 합니다.
|
||||||
|
func MustRegister() {
|
||||||
|
prometheus.MustRegister(
|
||||||
|
DTLSHandshakesTotal,
|
||||||
|
HTTPRequestsTotal,
|
||||||
|
HTTPRequestDurationSeconds,
|
||||||
|
ProxyErrorsTotal,
|
||||||
|
)
|
||||||
|
}
|
||||||
21
progress.md
21
progress.md
@@ -32,7 +32,7 @@ This document tracks implementation progress against the HopGate architecture an
|
|||||||
- `[type] short description [BREAK]` 형식, 타입 우선순위 정의, BREAK 규칙. (ko/en)
|
- `[type] short description [BREAK]` 형식, 타입 우선순위 정의, BREAK 규칙. (ko/en)
|
||||||
- Defines commit message format, type priorities, and `[BREAK]` convention. (en)
|
- Defines commit message format, type priorities, and `[BREAK]` convention. (en)
|
||||||
|
|
||||||
- 아키텍처 그림용 프롬프트: [`arkitecture.prompt`](arkitecture.prompt)
|
- 아키텍처 그림용 프롬프트: [`architecture.prompt`](images/architecture.prompt)
|
||||||
- 외부 도구(예: 나노바나나 Pro)가 참조할 상세 다이어그램 지침. (en 설명 위주)
|
- 외부 도구(예: 나노바나나 Pro)가 참조할 상세 다이어그램 지침. (en 설명 위주)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -213,20 +213,23 @@ This document tracks implementation progress against the HopGate architecture an
|
|||||||
- [x] 서버 main 에 ACME 기반 `*tls.Config` 주입
|
- [x] 서버 main 에 ACME 기반 `*tls.Config` 주입
|
||||||
- DTLS / HTTPS 리스너에 ACME 인증서 적용 (Debug 모드에서는 DTLS 에 self-signed, HTTPS 에 ACME 사용).
|
- DTLS / HTTPS 리스너에 ACME 인증서 적용 (Debug 모드에서는 DTLS 에 self-signed, HTTPS 에 ACME 사용).
|
||||||
|
|
||||||
- [ ] TLS-ALPN-01 챌린지 및 운영 전략 보완
|
- [ ] ACME 고급 기능 및 운영 전략 보완
|
||||||
- 필요 시 TLS-ALPN-01 챌린지 처리 로직 추가.
|
- TLS-ALPN-01 챌린지 지원 여부 검토 및 필요 시 lego 설정/핸들러 추가.
|
||||||
- 운영/테스트 환경 분리에 대한 문서화 및 구성 정교화.
|
- 인증서 발급/갱신 실패 시 재시도/백오프 및 경고 로그/알림을 포함한 에러 처리 전략 정의.
|
||||||
|
- Debug(스테이징 CA) / Production(실 CA) 환경 전환 플로우와 도메인/환경별 ACME 설정 매트릭스를 문서화.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.5 Observability / 관측성
|
### 3.5 Observability / 관측성
|
||||||
|
|
||||||
- [ ] Prometheus 메트릭 노출
|
- [ ] Prometheus 메트릭 노출 및 서버 wiring
|
||||||
- `/metrics` 엔드포인트 추가.
|
- `cmd/server/main.go` 에 Prometheus `/metrics` 엔드포인트 추가 (예: promhttp.Handler).
|
||||||
- DTLS 세션 수, 요청 수, 에러 수 등 카운터/게이지 정의.
|
- DTLS 세션 수, DTLS 핸드셰이크 성공/실패 수, HTTP/Proxy 요청 수 및 에러 수에 대한 카운터/게이지 메트릭 정의.
|
||||||
|
- 도메인, 클라이언트 ID, request_id 등의 라벨 설계 및 현재 구조적 로깅 필드와 일관성 유지.
|
||||||
|
|
||||||
- [ ] Loki/Grafana 대시보드 템플릿 초안 작성
|
- [ ] Loki/Grafana 대시보드 및 쿼리 예시
|
||||||
- 주요 필터(도메인, 클라이언트 ID, request_id) 기준 쿼리 예시.
|
- Loki/Promtail 구성을 가정한 주요 로그 쿼리 예시 정리(도메인, 클라이언트 ID, request_id 기준).
|
||||||
|
- Prometheus 메트릭 기반 기본 대시보드 템플릿 작성 (DTLS 상태, 프록시 트래픽, 에러율 등).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user