Add telemetry
This commit is contained in:
@@ -4,3 +4,5 @@
|
|||||||
anthropic-proxy
|
anthropic-proxy
|
||||||
result
|
result
|
||||||
config.yaml
|
config.yaml
|
||||||
|
|
||||||
|
vendor/**
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
port: 8082
|
port: 8082
|
||||||
|
|
||||||
|
# telemetry:
|
||||||
|
# endpoint: "localhost:4317" # OTLP gRPC endpoint (omit to disable export)
|
||||||
|
# insecure: true # disable TLS for local dev
|
||||||
|
# service_name: "anthropic-proxy"
|
||||||
|
# headers: # optional auth headers (e.g. Grafana Cloud)
|
||||||
|
# Authorization: "Basic ..."
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level: debug
|
level: debug
|
||||||
file: /home/fujin/.local/log/anthropic-proxy.log
|
file: /home/fujin/.local/log/anthropic-proxy.log
|
||||||
|
|||||||
@@ -42,6 +42,9 @@
|
|||||||
shellHook = ''
|
shellHook = ''
|
||||||
export GOPATH="$PWD/.go"
|
export GOPATH="$PWD/.go"
|
||||||
export PATH="$GOPATH/bin:$PATH"
|
export PATH="$GOPATH/bin:$PATH"
|
||||||
|
|
||||||
|
export ANTHROPIC_BASE_URL=http://localhost:8082
|
||||||
|
export ANTHROPIC_API_KEY=sk-cliproxy-fujin
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,23 +5,45 @@ go 1.26
|
|||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.12.0
|
github.com/gin-gonic/gin v1.12.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/refraction-networking/utls v1.8.2
|
||||||
|
github.com/rs/zerolog v1.35.0
|
||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
|
github.com/tidwall/sjson v1.2.5
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.68.0
|
||||||
|
go.opentelemetry.io/otel v1.43.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0
|
||||||
|
go.opentelemetry.io/otel/log v0.19.0
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0
|
||||||
|
go.opentelemetry.io/otel/sdk v1.43.0
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.19.0
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.43.0
|
||||||
|
golang.org/x/net v0.52.0
|
||||||
|
google.golang.org/grpc v1.80.0
|
||||||
|
gopkg.in/lumberjack.v2 v2.0.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||||
github.com/bytedance/sonic v1.15.0 // indirect
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
github.com/go-playground/validator/v10 v10.30.2 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.6 // indirect
|
||||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.17.6 // indirect
|
github.com/klauspost/compress v1.17.6 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
@@ -30,22 +52,25 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
github.com/refraction-networking/utls v1.8.2 // indirect
|
|
||||||
github.com/rs/zerolog v1.35.0 // indirect
|
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
golang.org/x/arch v0.22.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||||
|
golang.org/x/arch v0.25.0 // indirect
|
||||||
golang.org/x/crypto v0.49.0 // indirect
|
golang.org/x/crypto v0.49.0 // indirect
|
||||||
golang.org/x/net v0.52.0 // indirect
|
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||||
gopkg.in/lumberjack.v2 v2.0.0 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,39 +1,54 @@
|
|||||||
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
|
||||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
|
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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
||||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
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/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 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
|
||||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
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 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/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
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/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
|
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
|
||||||
@@ -55,8 +70,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
@@ -65,8 +80,8 @@ github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SA
|
|||||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
|
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
|
||||||
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@@ -95,34 +110,74 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY
|
|||||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.68.0 h1:5FXSL2s6afUC1bzNzl1iedZZ8yqR7GOhbCoEXtyeK6Q=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.68.0/go.mod h1:MdHW7tLtkeGJnR4TyOrnd5D0zUGZQB1l84uHCe8hRpE=
|
||||||
|
go.opentelemetry.io/contrib/propagators/b3 v1.43.0 h1:CETqV3QLLPTy5yNrqyMr41VnAOOD4lsRved7n4QG00A=
|
||||||
|
go.opentelemetry.io/contrib/propagators/b3 v1.43.0/go.mod h1:Q4mCiCdziYzpNR0g+6UqVotAlCDZdzz6L8jwY4knOrw=
|
||||||
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU=
|
||||||
|
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
|
||||||
|
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
|
||||||
|
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
|
||||||
|
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
|
||||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
|
||||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
|
||||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
|
||||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||||
|
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/lumberjack.v2 v2.0.0 h1:IDj6hi8KbNiPQ5VaYNFZ7dBJLF5LFeKvsFrWHjA5aq4=
|
gopkg.in/lumberjack.v2 v2.0.0 h1:IDj6hi8KbNiPQ5VaYNFZ7dBJLF5LFeKvsFrWHjA5aq4=
|
||||||
gopkg.in/lumberjack.v2 v2.0.0/go.mod h1:bp5nQ2kK/lLQSmTk29azj9+JB6bWci56xFn/lvd5GLI=
|
gopkg.in/lumberjack.v2 v2.0.0/go.mod h1:bp5nQ2kK/lLQSmTk29azj9+JB6bWci56xFn/lvd5GLI=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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 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=
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type Config struct {
|
|||||||
ClaudeBinary string `yaml:"claude_binary"`
|
ClaudeBinary string `yaml:"claude_binary"`
|
||||||
Sanitize SanitizeConfig `yaml:"sanitize"`
|
Sanitize SanitizeConfig `yaml:"sanitize"`
|
||||||
Logging LoggingConfig `yaml:"logging"`
|
Logging LoggingConfig `yaml:"logging"`
|
||||||
|
Telemetry TelemetryConfig `yaml:"telemetry"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SanitizeConfig struct {
|
type SanitizeConfig struct {
|
||||||
@@ -34,6 +35,15 @@ type ReplaceRule struct {
|
|||||||
Replace string `yaml:"replace"`
|
Replace string `yaml:"replace"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TelemetryConfig struct {
|
||||||
|
Endpoint string `yaml:"endpoint"`
|
||||||
|
Insecure bool `yaml:"insecure"`
|
||||||
|
ServiceName string `yaml:"service_name"`
|
||||||
|
Headers map[string]string `yaml:"headers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TelemetryConfig) ExportEnabled() bool { return t.Endpoint != "" }
|
||||||
|
|
||||||
type LoggingConfig struct {
|
type LoggingConfig struct {
|
||||||
Level string `yaml:"level"`
|
Level string `yaml:"level"`
|
||||||
File string `yaml:"file"`
|
File string `yaml:"file"`
|
||||||
@@ -76,6 +86,10 @@ func Load(path string) (*Config, error) {
|
|||||||
cfg.Logging.MaxAgeDays = 30
|
cfg.Logging.MaxAgeDays = 30
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.Telemetry.ServiceName == "" {
|
||||||
|
cfg.Telemetry.ServiceName = "anthropic-proxy"
|
||||||
|
}
|
||||||
|
|
||||||
// Check for deprecated claude_credentials field
|
// Check for deprecated claude_credentials field
|
||||||
var rawCfg map[string]interface{}
|
var rawCfg map[string]interface{}
|
||||||
if err := yaml.Unmarshal(data, &rawCfg); err == nil {
|
if err := yaml.Unmarshal(data, &rawCfg); err == nil {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package logging
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -28,7 +29,9 @@ type Config struct {
|
|||||||
// - File set: JSON → lumberjack rotating file
|
// - File set: JSON → lumberjack rotating file
|
||||||
// - File empty + TTY: colored ConsoleWriter → stderr
|
// - File empty + TTY: colored ConsoleWriter → stderr
|
||||||
// - File empty + not TTY: JSON → stderr (for systemd journal)
|
// - File empty + not TTY: JSON → stderr (for systemd journal)
|
||||||
func Setup(cfg Config) zerolog.Logger {
|
// Extra writers (e.g., OTLP log bridge) are added via io.MultiWriter so logs
|
||||||
|
// are written to both the primary destination and any extra writers.
|
||||||
|
func Setup(cfg Config, extraWriters ...io.Writer) zerolog.Logger {
|
||||||
// Parse log level
|
// Parse log level
|
||||||
level, err := zerolog.ParseLevel(cfg.Level)
|
level, err := zerolog.ParseLevel(cfg.Level)
|
||||||
if err != nil || cfg.Level == "" {
|
if err != nil || cfg.Level == "" {
|
||||||
@@ -48,20 +51,32 @@ func Setup(cfg Config) zerolog.Logger {
|
|||||||
MaxAge: cfg.MaxAgeDays,
|
MaxAge: cfg.MaxAgeDays,
|
||||||
Compress: cfg.Compress,
|
Compress: cfg.Compress,
|
||||||
}
|
}
|
||||||
logger = zerolog.New(jack).With().Timestamp().Caller().Logger()
|
var w io.Writer = jack
|
||||||
|
if len(extraWriters) > 0 {
|
||||||
|
w = io.MultiWriter(append([]io.Writer{jack}, extraWriters...)...)
|
||||||
|
}
|
||||||
|
logger = zerolog.New(w).With().Timestamp().Caller().Logger()
|
||||||
} else {
|
} else {
|
||||||
fi, err := os.Stderr.Stat()
|
fi, err := os.Stderr.Stat()
|
||||||
isTTY := err == nil && (fi.Mode()&os.ModeCharDevice) != 0
|
isTTY := err == nil && (fi.Mode()&os.ModeCharDevice) != 0
|
||||||
if isTTY {
|
if isTTY {
|
||||||
// Dev mode: colored console
|
// Dev mode: colored console (extra writers get JSON, console gets pretty)
|
||||||
cw := zerolog.ConsoleWriter{
|
cw := zerolog.ConsoleWriter{
|
||||||
Out: os.Stderr,
|
Out: os.Stderr,
|
||||||
TimeFormat: time.RFC3339,
|
TimeFormat: time.RFC3339,
|
||||||
}
|
}
|
||||||
logger = zerolog.New(cw).With().Timestamp().Caller().Logger()
|
var w io.Writer = cw
|
||||||
|
if len(extraWriters) > 0 {
|
||||||
|
w = io.MultiWriter(append([]io.Writer{cw}, extraWriters...)...)
|
||||||
|
}
|
||||||
|
logger = zerolog.New(w).With().Timestamp().Caller().Logger()
|
||||||
} else {
|
} else {
|
||||||
// Systemd journal: JSON to stderr
|
// Systemd journal: JSON to stderr
|
||||||
logger = zerolog.New(os.Stderr).With().Timestamp().Caller().Logger()
|
var w io.Writer = os.Stderr
|
||||||
|
if len(extraWriters) > 0 {
|
||||||
|
w = io.MultiWriter(append([]io.Writer{os.Stderr}, extraWriters...)...)
|
||||||
|
}
|
||||||
|
logger = zerolog.New(w).With().Timestamp().Caller().Logger()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+140
-8
@@ -9,9 +9,12 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
|
||||||
"github.com/fujin/anthropic-proxy/internal/auth"
|
"github.com/fujin/anthropic-proxy/internal/auth"
|
||||||
"github.com/fujin/anthropic-proxy/internal/logging"
|
"github.com/fujin/anthropic-proxy/internal/logging"
|
||||||
|
"github.com/fujin/anthropic-proxy/internal/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleMessages(pool *auth.Pool, profile *SniffedProfile, getSanitizer func() *Sanitizer) gin.HandlerFunc {
|
func HandleMessages(pool *auth.Pool, profile *SniffedProfile, getSanitizer func() *Sanitizer) gin.HandlerFunc {
|
||||||
@@ -55,10 +58,16 @@ func HandleMessages(pool *auth.Pool, profile *SniffedProfile, getSanitizer func(
|
|||||||
|
|
||||||
func handleNonStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool *auth.Pool, cred *auth.Credential, body []byte, originalBody []byte) {
|
func handleNonStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool *auth.Pool, cred *auth.Credential, body []byte, originalBody []byte) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
respBody, headers, statusCode, err := upstream.Execute(c.Request.Context(), cred, body)
|
|
||||||
if err != nil {
|
|
||||||
latencyMs := float64(time.Since(startTime).Milliseconds())
|
|
||||||
model := gjson.GetBytes(body, "model").String()
|
model := gjson.GetBytes(body, "model").String()
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
telemetry.RequestBodySize.Record(ctx, int64(len(body)),
|
||||||
|
metric.WithAttributes(attribute.String("model", model), attribute.Bool("stream", false)))
|
||||||
|
|
||||||
|
respBody, headers, statusCode, err := upstream.Execute(ctx, cred, body)
|
||||||
|
latencyMs := float64(time.Since(startTime).Milliseconds())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Err(err).
|
Err(err).
|
||||||
Str("credential", cred.Email).
|
Str("credential", cred.Email).
|
||||||
@@ -69,14 +78,39 @@ func handleNonStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, p
|
|||||||
Int("request_body_size", len(body)).
|
Int("request_body_size", len(body)).
|
||||||
Float64("latency_ms", latencyMs).
|
Float64("latency_ms", latencyMs).
|
||||||
Msg("upstream connection error")
|
Msg("upstream connection error")
|
||||||
|
|
||||||
|
telemetry.UpstreamErrors.Add(ctx, 1,
|
||||||
|
metric.WithAttributes(
|
||||||
|
attribute.String("error_type", "connection"),
|
||||||
|
attribute.String("credential", cred.Email),
|
||||||
|
attribute.Int("status_code", http.StatusBadGateway),
|
||||||
|
))
|
||||||
|
telemetry.RequestCounter.Add(ctx, 1,
|
||||||
|
metric.WithAttributes(
|
||||||
|
attribute.String("model", model),
|
||||||
|
attribute.Bool("stream", false),
|
||||||
|
attribute.Int("status_code", http.StatusBadGateway),
|
||||||
|
))
|
||||||
|
telemetry.RequestDuration.Record(ctx, latencyMs,
|
||||||
|
metric.WithAttributes(attribute.String("model", model), attribute.Bool("stream", false), attribute.Int("status_code", http.StatusBadGateway)))
|
||||||
|
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": "upstream request failed"})
|
c.JSON(http.StatusBadGateway, gin.H{"error": "upstream request failed"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attrs := []attribute.KeyValue{
|
||||||
|
attribute.String("model", model),
|
||||||
|
attribute.Bool("stream", false),
|
||||||
|
attribute.Int("status_code", statusCode),
|
||||||
|
}
|
||||||
|
|
||||||
|
telemetry.RequestCounter.Add(ctx, 1, metric.WithAttributes(attrs...))
|
||||||
|
telemetry.RequestDuration.Record(ctx, latencyMs, metric.WithAttributes(attrs...))
|
||||||
|
|
||||||
if statusCode >= 400 {
|
if statusCode >= 400 {
|
||||||
pool.MarkFailure(cred, statusCode)
|
pool.MarkFailure(cred, statusCode)
|
||||||
latencyMs := float64(time.Since(startTime).Milliseconds())
|
telemetry.CredentialCooldowns.Add(ctx, 1,
|
||||||
model := gjson.GetBytes(body, "model").String()
|
metric.WithAttributes(attribute.Int("status_code", statusCode)))
|
||||||
errorType := gjson.GetBytes(respBody, "error.type").String()
|
errorType := gjson.GetBytes(respBody, "error.type").String()
|
||||||
errorMessage := gjson.GetBytes(respBody, "error.message").String()
|
errorMessage := gjson.GetBytes(respBody, "error.message").String()
|
||||||
log.Error().
|
log.Error().
|
||||||
@@ -94,9 +128,33 @@ func handleNonStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, p
|
|||||||
Int("request_body_size", len(body)).
|
Int("request_body_size", len(body)).
|
||||||
Str("request_headers", logging.RedactHeaders(c.Request.Header)).
|
Str("request_headers", logging.RedactHeaders(c.Request.Header)).
|
||||||
Msg("upstream error")
|
Msg("upstream error")
|
||||||
|
|
||||||
|
telemetry.UpstreamErrors.Add(ctx, 1,
|
||||||
|
metric.WithAttributes(
|
||||||
|
attribute.Int("status_code", statusCode),
|
||||||
|
attribute.String("error_type", errorType),
|
||||||
|
attribute.String("credential", cred.Email),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
pool.MarkSuccess(cred)
|
pool.MarkSuccess(cred)
|
||||||
respBody = san.DesanitizeResponse(respBody)
|
respBody = san.DesanitizeResponse(respBody)
|
||||||
|
|
||||||
|
inputTokens := gjson.GetBytes(respBody, "usage.input_tokens").Int()
|
||||||
|
outputTokens := gjson.GetBytes(respBody, "usage.output_tokens").Int()
|
||||||
|
tokenAttrs := metric.WithAttributes(
|
||||||
|
attribute.String("model", model),
|
||||||
|
attribute.String("credential", cred.Email),
|
||||||
|
)
|
||||||
|
telemetry.TokensInput.Add(ctx, inputTokens, tokenAttrs)
|
||||||
|
telemetry.TokensOutput.Add(ctx, outputTokens, tokenAttrs)
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Int("status", statusCode).
|
||||||
|
Float64("latency_ms", latencyMs).
|
||||||
|
Str("model", model).
|
||||||
|
Int64("input_tokens", inputTokens).
|
||||||
|
Int64("output_tokens", outputTokens).
|
||||||
|
Msg("request completed")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, h := range []string{"Content-Type", "X-Request-Id"} {
|
for _, h := range []string{"Content-Type", "X-Request-Id"} {
|
||||||
@@ -110,10 +168,16 @@ func handleNonStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, p
|
|||||||
|
|
||||||
func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool *auth.Pool, cred *auth.Credential, body []byte, originalBody []byte) {
|
func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool *auth.Pool, cred *auth.Credential, body []byte, originalBody []byte) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
resp, err := upstream.ExecuteStream(c.Request.Context(), cred, body)
|
model := gjson.GetBytes(body, "model").String()
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
telemetry.StreamRequests.Add(ctx, 1, metric.WithAttributes(attribute.String("model", model)))
|
||||||
|
telemetry.RequestBodySize.Record(ctx, int64(len(body)),
|
||||||
|
metric.WithAttributes(attribute.String("model", model), attribute.Bool("stream", true)))
|
||||||
|
|
||||||
|
resp, err := upstream.ExecuteStream(ctx, cred, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
latencyMs := float64(time.Since(startTime).Milliseconds())
|
latencyMs := float64(time.Since(startTime).Milliseconds())
|
||||||
model := gjson.GetBytes(body, "model").String()
|
|
||||||
log.Error().
|
log.Error().
|
||||||
Err(err).
|
Err(err).
|
||||||
Str("credential", cred.Email).
|
Str("credential", cred.Email).
|
||||||
@@ -124,6 +188,22 @@ func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool
|
|||||||
Int("request_body_size", len(body)).
|
Int("request_body_size", len(body)).
|
||||||
Float64("latency_ms", latencyMs).
|
Float64("latency_ms", latencyMs).
|
||||||
Msg("upstream connection error")
|
Msg("upstream connection error")
|
||||||
|
|
||||||
|
telemetry.UpstreamErrors.Add(ctx, 1,
|
||||||
|
metric.WithAttributes(
|
||||||
|
attribute.String("error_type", "connection"),
|
||||||
|
attribute.String("credential", cred.Email),
|
||||||
|
attribute.Int("status_code", http.StatusBadGateway),
|
||||||
|
))
|
||||||
|
telemetry.RequestCounter.Add(ctx, 1,
|
||||||
|
metric.WithAttributes(
|
||||||
|
attribute.String("model", model),
|
||||||
|
attribute.Bool("stream", true),
|
||||||
|
attribute.Int("status_code", http.StatusBadGateway),
|
||||||
|
))
|
||||||
|
telemetry.RequestDuration.Record(ctx, latencyMs,
|
||||||
|
metric.WithAttributes(attribute.String("model", model), attribute.Bool("stream", true), attribute.Int("status_code", http.StatusBadGateway)))
|
||||||
|
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": "upstream stream request failed"})
|
c.JSON(http.StatusBadGateway, gin.H{"error": "upstream stream request failed"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -131,9 +211,10 @@ func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool
|
|||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
pool.MarkFailure(cred, resp.StatusCode)
|
pool.MarkFailure(cred, resp.StatusCode)
|
||||||
|
telemetry.CredentialCooldowns.Add(ctx, 1,
|
||||||
|
metric.WithAttributes(attribute.Int("status_code", resp.StatusCode)))
|
||||||
respBody, _ := io.ReadAll(resp.Body)
|
respBody, _ := io.ReadAll(resp.Body)
|
||||||
latencyMs := float64(time.Since(startTime).Milliseconds())
|
latencyMs := float64(time.Since(startTime).Milliseconds())
|
||||||
model := gjson.GetBytes(body, "model").String()
|
|
||||||
errorType := gjson.GetBytes(respBody, "error.type").String()
|
errorType := gjson.GetBytes(respBody, "error.type").String()
|
||||||
errorMessage := gjson.GetBytes(respBody, "error.message").String()
|
errorMessage := gjson.GetBytes(respBody, "error.message").String()
|
||||||
log.Error().
|
log.Error().
|
||||||
@@ -151,6 +232,21 @@ func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool
|
|||||||
Int("request_body_size", len(body)).
|
Int("request_body_size", len(body)).
|
||||||
Str("request_headers", logging.RedactHeaders(c.Request.Header)).
|
Str("request_headers", logging.RedactHeaders(c.Request.Header)).
|
||||||
Msg("upstream error")
|
Msg("upstream error")
|
||||||
|
|
||||||
|
attrs := []attribute.KeyValue{
|
||||||
|
attribute.String("model", model),
|
||||||
|
attribute.Bool("stream", true),
|
||||||
|
attribute.Int("status_code", resp.StatusCode),
|
||||||
|
}
|
||||||
|
telemetry.RequestCounter.Add(ctx, 1, metric.WithAttributes(attrs...))
|
||||||
|
telemetry.RequestDuration.Record(ctx, latencyMs, metric.WithAttributes(attrs...))
|
||||||
|
telemetry.UpstreamErrors.Add(ctx, 1,
|
||||||
|
metric.WithAttributes(
|
||||||
|
attribute.Int("status_code", resp.StatusCode),
|
||||||
|
attribute.String("error_type", errorType),
|
||||||
|
attribute.String("credential", cred.Email),
|
||||||
|
))
|
||||||
|
|
||||||
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), respBody)
|
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), respBody)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -169,13 +265,49 @@ func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var inputTokens, outputTokens int64
|
||||||
scanner := bufio.NewScanner(resp.Body)
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := san.DesanitizeStreamEvent(scanner.Text())
|
line := san.DesanitizeStreamEvent(scanner.Text())
|
||||||
c.Writer.WriteString(line + "\n")
|
c.Writer.WriteString(line + "\n")
|
||||||
flusher.Flush()
|
flusher.Flush()
|
||||||
|
|
||||||
|
// Extract token usage from message_delta event
|
||||||
|
if len(line) > 5 && line[:5] == "data:" {
|
||||||
|
data := line[5:]
|
||||||
|
if gjson.Get(data, "type").String() == "message_delta" {
|
||||||
|
inputTokens = gjson.Get(data, "usage.input_tokens").Int()
|
||||||
|
outputTokens = gjson.Get(data, "usage.output_tokens").Int()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
latencyMs := float64(time.Since(startTime).Milliseconds())
|
||||||
|
attrs := []attribute.KeyValue{
|
||||||
|
attribute.String("model", model),
|
||||||
|
attribute.Bool("stream", true),
|
||||||
|
attribute.Int("status_code", http.StatusOK),
|
||||||
|
}
|
||||||
|
telemetry.RequestCounter.Add(ctx, 1, metric.WithAttributes(attrs...))
|
||||||
|
telemetry.RequestDuration.Record(ctx, latencyMs, metric.WithAttributes(attrs...))
|
||||||
|
|
||||||
|
if inputTokens > 0 || outputTokens > 0 {
|
||||||
|
tokenAttrs := metric.WithAttributes(
|
||||||
|
attribute.String("model", model),
|
||||||
|
attribute.String("credential", cred.Email),
|
||||||
|
)
|
||||||
|
telemetry.TokensInput.Add(ctx, inputTokens, tokenAttrs)
|
||||||
|
telemetry.TokensOutput.Add(ctx, outputTokens, tokenAttrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Float64("latency_ms", latencyMs).
|
||||||
|
Str("model", model).
|
||||||
|
Bool("stream", true).
|
||||||
|
Int64("input_tokens", inputTokens).
|
||||||
|
Int64("output_tokens", outputTokens).
|
||||||
|
Msg("stream completed")
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
log.Error().Err(err).Msg("stream scan error")
|
log.Error().Err(err).Msg("stream scan error")
|
||||||
|
|||||||
@@ -51,6 +51,15 @@ func (u *UpstreamClient) applyHeaders(req *http.Request, token string, streaming
|
|||||||
req.Header.Del("x-api-key")
|
req.Header.Del("x-api-key")
|
||||||
if strings.HasPrefix(token, "sk-ant-oat") {
|
if strings.HasPrefix(token, "sk-ant-oat") {
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
// OAuth tokens require this beta flag — without it the API rejects with 401
|
||||||
|
existing := req.Header.Get("anthropic-beta")
|
||||||
|
if !strings.Contains(existing, "oauth-2025-04-20") {
|
||||||
|
if existing == "" {
|
||||||
|
req.Header.Set("anthropic-beta", "oauth-2025-04-20")
|
||||||
|
} else {
|
||||||
|
req.Header.Set("anthropic-beta", existing+",oauth-2025-04-20")
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
req.Header.Set("x-api-key", token)
|
req.Header.Set("x-api-key", token)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/fujin/anthropic-proxy/internal/config"
|
"github.com/fujin/anthropic-proxy/internal/config"
|
||||||
"github.com/fujin/anthropic-proxy/internal/logging"
|
"github.com/fujin/anthropic-proxy/internal/logging"
|
||||||
"github.com/fujin/anthropic-proxy/internal/proxy"
|
"github.com/fujin/anthropic-proxy/internal/proxy"
|
||||||
|
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
@@ -37,6 +38,9 @@ func New(cfg *config.Config, pool *auth.Pool, profile *proxy.SniffedProfile) *Se
|
|||||||
engine := gin.New()
|
engine := gin.New()
|
||||||
engine.Use(gin.Recovery())
|
engine.Use(gin.Recovery())
|
||||||
engine.Use(corsMiddleware())
|
engine.Use(corsMiddleware())
|
||||||
|
if cfg.Telemetry.ExportEnabled() {
|
||||||
|
engine.Use(otelgin.Middleware(cfg.Telemetry.ServiceName))
|
||||||
|
}
|
||||||
engine.Use(s.authMiddleware())
|
engine.Use(s.authMiddleware())
|
||||||
engine.Use(logging.GinRequestLogger())
|
engine.Use(logging.GinRequestLogger())
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package telemetry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
otellog "go.opentelemetry.io/otel/log"
|
||||||
|
sdklog "go.opentelemetry.io/otel/sdk/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogBridge implements io.Writer and forwards zerolog JSON lines to the
|
||||||
|
// OTel LoggerProvider. It is used as an extra writer in zerolog's MultiWriter
|
||||||
|
// so that logs go to both file and OTLP.
|
||||||
|
type LogBridge struct {
|
||||||
|
provider *sdklog.LoggerProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LogBridge) Write(p []byte) (n int, err error) {
|
||||||
|
var entry map[string]interface{}
|
||||||
|
if err := json.Unmarshal(p, &entry); err != nil {
|
||||||
|
return len(p), nil // skip malformed lines
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := b.provider.Logger("zerolog")
|
||||||
|
|
||||||
|
var rec otellog.Record
|
||||||
|
rec.SetTimestamp(time.Now())
|
||||||
|
|
||||||
|
if msg, ok := entry["message"].(string); ok {
|
||||||
|
rec.SetBody(otellog.StringValue(msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
if lvl, ok := entry["level"].(string); ok {
|
||||||
|
rec.SetSeverity(mapSeverity(lvl))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward all fields as attributes
|
||||||
|
attrs := make([]otellog.KeyValue, 0, len(entry))
|
||||||
|
for k, v := range entry {
|
||||||
|
if k == "message" || k == "level" || k == "time" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch val := v.(type) {
|
||||||
|
case string:
|
||||||
|
attrs = append(attrs, otellog.String(k, val))
|
||||||
|
case float64:
|
||||||
|
attrs = append(attrs, otellog.Float64(k, val))
|
||||||
|
case bool:
|
||||||
|
attrs = append(attrs, otellog.Bool(k, val))
|
||||||
|
default:
|
||||||
|
b, _ := json.Marshal(val)
|
||||||
|
attrs = append(attrs, otellog.String(k, string(b)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rec.AddAttributes(attrs...)
|
||||||
|
|
||||||
|
logger.Emit(context.Background(), rec)
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapSeverity(level string) otellog.Severity {
|
||||||
|
switch level {
|
||||||
|
case "trace":
|
||||||
|
return otellog.SeverityTrace
|
||||||
|
case "debug":
|
||||||
|
return otellog.SeverityDebug
|
||||||
|
case "info":
|
||||||
|
return otellog.SeverityInfo
|
||||||
|
case "warn", "warning":
|
||||||
|
return otellog.SeverityWarn
|
||||||
|
case "error":
|
||||||
|
return otellog.SeverityError
|
||||||
|
case "fatal":
|
||||||
|
return otellog.SeverityFatal
|
||||||
|
case "panic":
|
||||||
|
return otellog.SeverityFatal2
|
||||||
|
default:
|
||||||
|
return otellog.SeverityInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package telemetry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
RequestCounter metric.Int64Counter
|
||||||
|
RequestDuration metric.Float64Histogram
|
||||||
|
RequestBodySize metric.Int64Histogram
|
||||||
|
UpstreamErrors metric.Int64Counter
|
||||||
|
TokensInput metric.Int64Counter
|
||||||
|
TokensOutput metric.Int64Counter
|
||||||
|
CredentialCooldowns metric.Int64Counter
|
||||||
|
ActiveCredentials metric.Int64UpDownCounter
|
||||||
|
StreamRequests metric.Int64Counter
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitMetrics creates all metric instruments from the given meter.
|
||||||
|
func InitMetrics(meter metric.Meter) {
|
||||||
|
RequestCounter, _ = meter.Int64Counter("proxy.request.count",
|
||||||
|
metric.WithDescription("Total proxy requests"),
|
||||||
|
)
|
||||||
|
RequestDuration, _ = meter.Float64Histogram("proxy.request.duration_ms",
|
||||||
|
metric.WithDescription("Request latency in milliseconds"),
|
||||||
|
metric.WithUnit("ms"),
|
||||||
|
)
|
||||||
|
RequestBodySize, _ = meter.Int64Histogram("proxy.request.body_size_bytes",
|
||||||
|
metric.WithDescription("Request body size in bytes"),
|
||||||
|
metric.WithUnit("By"),
|
||||||
|
)
|
||||||
|
UpstreamErrors, _ = meter.Int64Counter("proxy.upstream.errors",
|
||||||
|
metric.WithDescription("Upstream error count"),
|
||||||
|
)
|
||||||
|
TokensInput, _ = meter.Int64Counter("proxy.tokens.input",
|
||||||
|
metric.WithDescription("Input tokens consumed"),
|
||||||
|
)
|
||||||
|
TokensOutput, _ = meter.Int64Counter("proxy.tokens.output",
|
||||||
|
metric.WithDescription("Output tokens consumed"),
|
||||||
|
)
|
||||||
|
CredentialCooldowns, _ = meter.Int64Counter("proxy.credential.cooldowns",
|
||||||
|
metric.WithDescription("Credential cooldown activations"),
|
||||||
|
)
|
||||||
|
ActiveCredentials, _ = meter.Int64UpDownCounter("proxy.credential.active",
|
||||||
|
metric.WithDescription("Currently active (non-cooldown) credentials"),
|
||||||
|
)
|
||||||
|
StreamRequests, _ = meter.Int64Counter("proxy.stream.requests",
|
||||||
|
metric.WithDescription("Streaming request count"),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package telemetry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/fujin/anthropic-proxy/internal/config"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||||
|
otellog "go.opentelemetry.io/otel/log/global"
|
||||||
|
"go.opentelemetry.io/otel/sdk/log"
|
||||||
|
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
"go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup initializes OpenTelemetry providers. It always creates a MeterProvider
|
||||||
|
// so metrics can be recorded in-process. When cfg.ExportEnabled(), OTLP gRPC
|
||||||
|
// exporters are additionally configured to push to the LGTM stack.
|
||||||
|
// Returns a shutdown function and an optional io.Writer for the log bridge.
|
||||||
|
func Setup(ctx context.Context, cfg config.TelemetryConfig) (shutdown func(context.Context) error, logWriter io.Writer, err error) {
|
||||||
|
res, err := resource.New(ctx,
|
||||||
|
resource.WithAttributes(
|
||||||
|
semconv.ServiceName(cfg.ServiceName),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.ExportEnabled() {
|
||||||
|
// No export — set up in-memory meter provider only so metric
|
||||||
|
// instruments are valid (they just don't export anywhere).
|
||||||
|
mp := sdkmetric.NewMeterProvider(sdkmetric.WithResource(res))
|
||||||
|
otel.SetMeterProvider(mp)
|
||||||
|
InitMetrics(mp.Meter(cfg.ServiceName))
|
||||||
|
return func(ctx context.Context) error { return mp.Shutdown(ctx) }, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build exporter options
|
||||||
|
traceOpts := []otlptracegrpc.Option{otlptracegrpc.WithEndpoint(cfg.Endpoint)}
|
||||||
|
metricOpts := []otlpmetricgrpc.Option{
|
||||||
|
otlpmetricgrpc.WithEndpoint(cfg.Endpoint),
|
||||||
|
otlpmetricgrpc.WithTemporalitySelector(sdkmetric.CumulativeTemporalitySelector),
|
||||||
|
}
|
||||||
|
logOpts := []otlploggrpc.Option{otlploggrpc.WithEndpoint(cfg.Endpoint)}
|
||||||
|
if cfg.Insecure {
|
||||||
|
traceOpts = append(traceOpts, otlptracegrpc.WithInsecure())
|
||||||
|
metricOpts = append(metricOpts, otlpmetricgrpc.WithInsecure())
|
||||||
|
logOpts = append(logOpts, otlploggrpc.WithInsecure())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trace exporter
|
||||||
|
traceExp, err := otlptracegrpc.New(ctx, traceOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
tp := trace.NewTracerProvider(
|
||||||
|
trace.WithBatcher(traceExp),
|
||||||
|
trace.WithResource(res),
|
||||||
|
)
|
||||||
|
otel.SetTracerProvider(tp)
|
||||||
|
|
||||||
|
// Metric exporter
|
||||||
|
metricExp, err := otlpmetricgrpc.New(ctx, metricOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
mp := sdkmetric.NewMeterProvider(
|
||||||
|
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExp)),
|
||||||
|
sdkmetric.WithResource(res),
|
||||||
|
)
|
||||||
|
otel.SetMeterProvider(mp)
|
||||||
|
InitMetrics(mp.Meter(cfg.ServiceName))
|
||||||
|
|
||||||
|
// Log exporter
|
||||||
|
logExp, err := otlploggrpc.New(ctx, logOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
lp := log.NewLoggerProvider(
|
||||||
|
log.WithProcessor(log.NewBatchProcessor(logExp)),
|
||||||
|
log.WithResource(res),
|
||||||
|
)
|
||||||
|
otellog.SetLoggerProvider(lp)
|
||||||
|
|
||||||
|
bridge := &LogBridge{provider: lp}
|
||||||
|
|
||||||
|
shutdownFn := func(ctx context.Context) error {
|
||||||
|
var firstErr error
|
||||||
|
if e := tp.Shutdown(ctx); e != nil && firstErr == nil {
|
||||||
|
firstErr = e
|
||||||
|
}
|
||||||
|
if e := mp.Shutdown(ctx); e != nil && firstErr == nil {
|
||||||
|
firstErr = e
|
||||||
|
}
|
||||||
|
if e := lp.Shutdown(ctx); e != nil && firstErr == nil {
|
||||||
|
firstErr = e
|
||||||
|
}
|
||||||
|
return firstErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return shutdownFn, bridge, nil
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -14,6 +15,7 @@ import (
|
|||||||
"github.com/fujin/anthropic-proxy/internal/logging"
|
"github.com/fujin/anthropic-proxy/internal/logging"
|
||||||
"github.com/fujin/anthropic-proxy/internal/proxy"
|
"github.com/fujin/anthropic-proxy/internal/proxy"
|
||||||
"github.com/fujin/anthropic-proxy/internal/server"
|
"github.com/fujin/anthropic-proxy/internal/server"
|
||||||
|
"github.com/fujin/anthropic-proxy/internal/telemetry"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,6 +25,18 @@ func run() error {
|
|||||||
return fmt.Errorf("load config: %w", err)
|
return fmt.Errorf("load config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize telemetry (metrics always active; OTLP export when endpoint set)
|
||||||
|
telemetryShutdown, logBridge, err := telemetry.Setup(context.Background(), cfg.Telemetry)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("telemetry setup: %w", err)
|
||||||
|
}
|
||||||
|
defer telemetryShutdown(context.Background())
|
||||||
|
|
||||||
|
var extraWriters []io.Writer
|
||||||
|
if logBridge != nil {
|
||||||
|
extraWriters = append(extraWriters, logBridge)
|
||||||
|
}
|
||||||
|
|
||||||
logging.Setup(logging.Config{
|
logging.Setup(logging.Config{
|
||||||
Level: cfg.Logging.Level,
|
Level: cfg.Logging.Level,
|
||||||
File: cfg.Logging.File,
|
File: cfg.Logging.File,
|
||||||
@@ -30,7 +44,7 @@ func run() error {
|
|||||||
MaxBackups: cfg.Logging.MaxBackups,
|
MaxBackups: cfg.Logging.MaxBackups,
|
||||||
MaxAgeDays: cfg.Logging.MaxAgeDays,
|
MaxAgeDays: cfg.Logging.MaxAgeDays,
|
||||||
Compress: cfg.Logging.Compress,
|
Compress: cfg.Logging.Compress,
|
||||||
})
|
}, extraWriters...)
|
||||||
|
|
||||||
// Load credentials from ~/.claude/.credentials.json
|
// Load credentials from ~/.claude/.credentials.json
|
||||||
creds, err := config.LoadDefaultCredentials()
|
creds, err := config.LoadDefaultCredentials()
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ buildGoModule rec {
|
|||||||
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
vendorHash = "sha256-xKztaGlelw7OI/6RJkkepHmLLH+dCCqYXE71C+y3PwI=";
|
vendorHash = "sha256-8pq4GYFjOfYcYLcZSuXMWn77RUxVGP18AcyzIJGbKf4=";
|
||||||
|
|
||||||
meta = with lib; {
|
meta = with lib; {
|
||||||
description = "Reverse proxy that lets OpenCode (and similar tools) use a Claude subscription instead of an API key.";
|
description = "Reverse proxy that lets OpenCode (and similar tools) use a Claude subscription instead of an API key.";
|
||||||
|
|||||||
Reference in New Issue
Block a user