ai cooking
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

app insights (#287)

* app insights

* middleware

* paul is pendantic about closers

* move parsing test to new file

* move more tests out

* simplify parsing

* slightly better closer

* Revert "simplify parsing"

This reverts commit f543b8c230cf6e8e5f4f1d1539c19200ea249560.

* comment

---------

Co-authored-by: paul miller <paul.miller>

authored by

Paul Miller
paul miller
and committed by
GitHub
1234ff7c d0adb197

+354 -19
+44 -14
cmd/careme/main.go
··· 9 9 "context" 10 10 _ "embed" 11 11 "flag" 12 + "fmt" 12 13 "log" 13 14 "log/slog" 14 15 "os" 15 16 16 - multi "github.com/samber/slog-multi" 17 + "github.com/openclosed-dev/slogan/appinsights" 18 + multi "github.com/samber/slog-multi" //this is getting a native version in newest golang 17 19 ) 20 + 21 + const appInsightsConnectionStringEnv = "APPLICATIONINSIGHTS_CONNECTION_STRING" 18 22 19 23 func main() { 20 24 var serve, mailer bool ··· 39 43 } 40 44 41 45 logcfg := logsink.ConfigFromEnv("logs") 42 - if logcfg.Enabled() { 43 - handler, closer, err := logsink.NewJson(ctx, logcfg) 44 - if err != nil { 45 - log.Fatalf("failed to create logsink: %v", err) 46 - } 47 - defer func() { 48 - if err := closer.Close(); err != nil { 49 - slog.Error("failed to close logsink", "error", err) 50 - } 51 - }() 52 - slog.SetDefault(slog.New(multi.Fanout(handler, slog.NewTextHandler(os.Stdout, nil)))) 53 - // log.SetOutput(os.Stdout) // https://github.com/golang/go/issues/61892 54 - 46 + close, err := configureLogger(ctx, logcfg) 47 + if err != nil { 48 + log.Fatalf("failed to configure logging: %v", err) 55 49 } 50 + defer close() 56 51 57 52 static.Init() 58 53 if err := templates.Init(cfg, static.TailwindAssetPath); err != nil { ··· 73 68 log.Fatalf("server error: %v", err) 74 69 } 75 70 } 71 + 72 + func configureLogger(ctx context.Context, logcfg logsink.Config) (func(), error) { 73 + handlers := make([]slog.Handler, 0, 3) 74 + var closers []func() //neat to be io.Closer 75 + if logcfg.Enabled() { 76 + handler, closer, err := logsink.NewJson(ctx, logcfg) 77 + if err != nil { 78 + return nil, fmt.Errorf("create logsink: %w", err) 79 + } 80 + handlers = append(handlers, handler) 81 + closers = append(closers, func() { 82 + if err := closer.Close(); err != nil { 83 + slog.Error("failed to close logsink", "error", err) 84 + } 85 + }) 86 + } 87 + if connectionString := os.Getenv(appInsightsConnectionStringEnv); connectionString != "" { 88 + handler, err := appinsights.NewHandler(connectionString, nil) 89 + if err != nil { 90 + return nil, fmt.Errorf("create app insights handler: %w", err) 91 + } 92 + handlers = append(handlers, handler) 93 + closers = append(closers, handler.Close) 94 + } 95 + 96 + close := func() { 97 + for _, closer := range closers { 98 + closer() 99 + } 100 + } 101 + 102 + handlers = append(handlers, slog.NewTextHandler(os.Stdout, nil)) 103 + slog.SetDefault(slog.New(multi.Fanout(handlers...))) 104 + return close, nil 105 + }
+108 -5
cmd/careme/middleware.go
··· 1 1 package main 2 2 3 3 import ( 4 + "errors" 4 5 "log/slog" 5 6 "net/http" 7 + "net/url" 8 + "os" 6 9 "runtime/debug" 10 + "strconv" 11 + "strings" 7 12 "time" 8 13 9 14 "github.com/clerk/clerk-sdk-go/v2" 15 + azureappinsights "github.com/microsoft/ApplicationInsights-Go/appinsights" 10 16 ) 11 17 12 18 type logger struct { ··· 40 46 slog.Info("request", "method", r.Method, "url", r.URL.Path, "query", r.URL.Query(), "response", lrw.statusCode, "user", user, "form", r.Form, "duration", time.Since(start)) 41 47 } 42 48 49 + type requestTracker interface { 50 + TrackRequest(method, url string, duration time.Duration, responseCode string) 51 + } 52 + 53 + type appInsightsTracker struct { 54 + http.Handler 55 + tracker requestTracker 56 + } 57 + 58 + const appInsightsIngestionPath = "/v2/track" 59 + 60 + func (a *appInsightsTracker) ServeHTTP(w http.ResponseWriter, r *http.Request) { 61 + start := time.Now() 62 + lrw := &loggingResponseWriter{w, http.StatusOK} 63 + a.Handler.ServeHTTP(lrw, r) 64 + 65 + if r.URL.Path == "/ready" { 66 + return 67 + } 68 + 69 + a.tracker.TrackRequest(r.Method, r.URL.String(), time.Since(start), strconv.Itoa(lrw.statusCode)) 70 + } 71 + 72 + func newAppInsightsTracker(next http.Handler, connectionString string) (http.Handler, error) { 73 + client, err := newAppInsightsTelemetryClient(connectionString) 74 + if err != nil { 75 + return nil, err 76 + } 77 + return &appInsightsTracker{ 78 + Handler: next, 79 + tracker: client, 80 + }, nil 81 + } 82 + 83 + func newAppInsightsTrackerFromEnv(next http.Handler) http.Handler { 84 + connectionString := os.Getenv(appInsightsConnectionStringEnv) 85 + if connectionString == "" { 86 + return next 87 + } 88 + 89 + handler, err := newAppInsightsTracker(next, connectionString) 90 + if err != nil { 91 + slog.Error("failed to configure app insights request tracking", "error", err) 92 + return next 93 + } 94 + 95 + return handler 96 + } 97 + 98 + func newAppInsightsTelemetryClient(connectionString string) (azureappinsights.TelemetryClient, error) { 99 + cfg, err := parseAppInsightsConnectionString(connectionString) 100 + if err != nil { 101 + return nil, err 102 + } 103 + return azureappinsights.NewTelemetryClientFromConfig(cfg), nil 104 + } 105 + 106 + // suprise there is not a parse function here. Chatgpt things github.com/Azure/go-autorest/autorest/azure.ParseConnectionString but codex coudln't find it 107 + func parseAppInsightsConnectionString(connectionString string) (*azureappinsights.TelemetryConfiguration, error) { 108 + connectionString = strings.TrimSpace(connectionString) 109 + if connectionString == "" { 110 + return nil, errors.New("connection string is empty") 111 + } 112 + 113 + var instrumentationKey string 114 + var ingestionEndpoint string 115 + 116 + for _, value := range strings.Split(connectionString, ";") { 117 + pair := strings.SplitN(value, "=", 2) 118 + if len(pair) != 2 { 119 + continue 120 + } 121 + switch pair[0] { 122 + case "InstrumentationKey": 123 + instrumentationKey = pair[1] 124 + case "IngestionEndpoint": 125 + ingestionEndpoint = pair[1] 126 + } 127 + } 128 + 129 + if instrumentationKey == "" { 130 + return nil, errors.New("instrumentation key is missing") 131 + } 132 + if ingestionEndpoint == "" { 133 + return nil, errors.New("ingestion endpoint is missing") 134 + } 135 + 136 + ingestionURL, err := url.Parse(ingestionEndpoint) 137 + if err != nil { 138 + return nil, err 139 + } 140 + 141 + cfg := azureappinsights.NewTelemetryConfiguration(instrumentationKey) 142 + ingestionURL.Path = appInsightsIngestionPath 143 + cfg.EndpointUrl = ingestionURL.String() 144 + return cfg, nil 145 + } 146 + 43 147 type recoverer struct { 44 148 http.Handler 45 149 } 46 150 47 151 func (r *recoverer) ServeHTTP(w http.ResponseWriter, req *http.Request) { 152 + //app insights could also track this https://github.com/microsoft/ApplicationInsights-Go?tab=readme-ov-file#exceptions 48 153 defer func() { 49 154 if err := recover(); err != nil { 50 155 slog.ErrorContext(req.Context(), "panic recovered", "error", err, "stack", debug.Stack()) ··· 55 160 } 56 161 57 162 func WithMiddleware(h http.Handler) http.Handler { 58 - return &logger{ 59 - &recoverer{ 60 - h, 61 - }, 62 - } 163 + h = &recoverer{h} 164 + h = newAppInsightsTrackerFromEnv(h) 165 + return &logger{h} 63 166 }
+181
cmd/careme/middleware_test.go
··· 1 + package main 2 + 3 + import ( 4 + "net/http" 5 + "net/http/httptest" 6 + "strings" 7 + "testing" 8 + "time" 9 + ) 10 + 11 + type trackedRequest struct { 12 + method string 13 + url string 14 + duration time.Duration 15 + responseCode string 16 + } 17 + 18 + type fakeRequestTracker struct { 19 + calls []trackedRequest 20 + } 21 + 22 + func (f *fakeRequestTracker) TrackRequest(method, url string, duration time.Duration, responseCode string) { 23 + f.calls = append(f.calls, trackedRequest{ 24 + method: method, 25 + url: url, 26 + duration: duration, 27 + responseCode: responseCode, 28 + }) 29 + } 30 + 31 + func TestAppInsightsTrackerTracksResponseCode(t *testing.T) { 32 + tracker := &fakeRequestTracker{} 33 + mw := &appInsightsTracker{ 34 + Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 35 + w.WriteHeader(http.StatusCreated) 36 + }), 37 + tracker: tracker, 38 + } 39 + 40 + req := httptest.NewRequest(http.MethodPost, "https://careme.cooking/recipes?vegan=true", nil) 41 + rec := httptest.NewRecorder() 42 + mw.ServeHTTP(rec, req) 43 + 44 + if len(tracker.calls) != 1 { 45 + t.Fatalf("expected 1 tracked request, got %d", len(tracker.calls)) 46 + } 47 + 48 + call := tracker.calls[0] 49 + if call.method != http.MethodPost { 50 + t.Fatalf("expected method %q, got %q", http.MethodPost, call.method) 51 + } 52 + if call.url != req.URL.String() { 53 + t.Fatalf("expected url %q, got %q", req.URL.String(), call.url) 54 + } 55 + if call.responseCode != "201" { 56 + t.Fatalf("expected response code 201, got %q", call.responseCode) 57 + } 58 + if call.duration <= 0 { 59 + t.Fatalf("expected positive duration, got %s", call.duration) 60 + } 61 + } 62 + 63 + func TestAppInsightsTrackerDefaultsStatusCodeTo200(t *testing.T) { 64 + tracker := &fakeRequestTracker{} 65 + mw := &appInsightsTracker{ 66 + Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 67 + _, _ = w.Write([]byte("ok")) 68 + }), 69 + tracker: tracker, 70 + } 71 + 72 + req := httptest.NewRequest(http.MethodGet, "https://careme.cooking/about", nil) 73 + rec := httptest.NewRecorder() 74 + mw.ServeHTTP(rec, req) 75 + 76 + if len(tracker.calls) != 1 { 77 + t.Fatalf("expected 1 tracked request, got %d", len(tracker.calls)) 78 + } 79 + if tracker.calls[0].responseCode != "200" { 80 + t.Fatalf("expected response code 200, got %q", tracker.calls[0].responseCode) 81 + } 82 + } 83 + 84 + func TestAppInsightsTrackerSkipsReady(t *testing.T) { 85 + tracker := &fakeRequestTracker{} 86 + mw := &appInsightsTracker{ 87 + Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 88 + w.WriteHeader(http.StatusOK) 89 + }), 90 + tracker: tracker, 91 + } 92 + 93 + req := httptest.NewRequest(http.MethodGet, "https://careme.cooking/ready", nil) 94 + rec := httptest.NewRecorder() 95 + mw.ServeHTTP(rec, req) 96 + 97 + if len(tracker.calls) != 0 { 98 + t.Fatalf("expected 0 tracked requests for /ready, got %d", len(tracker.calls)) 99 + } 100 + } 101 + 102 + func TestAppInsightsTrackerTracksRecoveredPanicAs500(t *testing.T) { 103 + tracker := &fakeRequestTracker{} 104 + mw := &appInsightsTracker{ 105 + Handler: &recoverer{ 106 + Handler: http.HandlerFunc(func(http.ResponseWriter, *http.Request) { 107 + panic("boom") 108 + }), 109 + }, 110 + tracker: tracker, 111 + } 112 + 113 + req := httptest.NewRequest(http.MethodGet, "https://careme.cooking/panic", nil) 114 + rec := httptest.NewRecorder() 115 + mw.ServeHTTP(rec, req) 116 + 117 + if rec.Code != http.StatusInternalServerError { 118 + t.Fatalf("expected status 500, got %d", rec.Code) 119 + } 120 + if len(tracker.calls) != 1 { 121 + t.Fatalf("expected 1 tracked request, got %d", len(tracker.calls)) 122 + } 123 + if tracker.calls[0].responseCode != "500" { 124 + t.Fatalf("expected response code 500, got %q", tracker.calls[0].responseCode) 125 + } 126 + } 127 + 128 + func TestParseAppInsightsConnectionString(t *testing.T) { 129 + connectionString := "InstrumentationKey=test-key;IngestionEndpoint=https://westus3-1.in.applicationinsights.azure.com/;LiveEndpoint=https://westus3.livediagnostics.monitor.azure.com/;ApplicationId=app-id" 130 + cfg, err := parseAppInsightsConnectionString(connectionString) 131 + if err != nil { 132 + t.Fatalf("unexpected error: %v", err) 133 + } 134 + if cfg.InstrumentationKey != "test-key" { 135 + t.Fatalf("expected instrumentation key test-key, got %q", cfg.InstrumentationKey) 136 + } 137 + if cfg.EndpointUrl != "https://westus3-1.in.applicationinsights.azure.com/v2/track" { 138 + t.Fatalf("unexpected ingestion endpoint: %q", cfg.EndpointUrl) 139 + } 140 + } 141 + 142 + func TestParseAppInsightsConnectionStringErrors(t *testing.T) { 143 + testCases := []struct { 144 + name string 145 + value string 146 + wantErrText string 147 + }{ 148 + { 149 + name: "empty", 150 + value: "", 151 + wantErrText: "connection string is empty", 152 + }, 153 + { 154 + name: "missing instrumentation key", 155 + value: "IngestionEndpoint=https://westus3-1.in.applicationinsights.azure.com/", 156 + wantErrText: "instrumentation key is missing", 157 + }, 158 + { 159 + name: "missing ingestion endpoint", 160 + value: "InstrumentationKey=test-key", 161 + wantErrText: "ingestion endpoint is missing", 162 + }, 163 + { 164 + name: "bad ingestion endpoint", 165 + value: "InstrumentationKey=test-key;IngestionEndpoint=:bad://", 166 + wantErrText: "missing protocol scheme", 167 + }, 168 + } 169 + 170 + for _, tc := range testCases { 171 + t.Run(tc.name, func(t *testing.T) { 172 + _, err := parseAppInsightsConnectionString(tc.value) 173 + if err == nil { 174 + t.Fatalf("expected error containing %q", tc.wantErrText) 175 + } 176 + if !strings.Contains(err.Error(), tc.wantErrText) { 177 + t.Fatalf("expected error containing %q, got %q", tc.wantErrText, err.Error()) 178 + } 179 + }) 180 + } 181 + }
+5
deploy/deploy.yaml
··· 38 38 value: "xDzACMu074AcEI_x3dhC" 39 39 - name: ADMIN_EMAILS 40 40 value: "paul.miller@gmail.com" 41 + - name: APPLICATIONINSIGHTS_CONNECTION_STRING 42 + value: "InstrumentationKey=a532fcc7-5098-4f44-8dde-ff2f32d6a59b;IngestionEndpoint=https://westus3-1.in.applicationinsights.azure.com/;LiveEndpoint=https://westus3.livediagnostics.monitor.azure.com/;ApplicationId=fdc94780-6135-4a29-980e-ab114a402e58" 41 43 volumeMounts: 42 44 - name: recipes 43 45 mountPath: /recipes ··· 97 99 envFrom: 98 100 - secretRef: 99 101 name: careme-secrets3 102 + env: 103 + - name: APPLICATIONINSIGHTS_CONNECTION_STRING 104 + value: "InstrumentationKey=a532fcc7-5098-4f44-8dde-ff2f32d6a59b;IngestionEndpoint=https://westus3-1.in.applicationinsights.azure.com/;LiveEndpoint=https://westus3.livediagnostics.monitor.azure.com/;ApplicationId=fdc94780-6135-4a29-980e-ab114a402e58" 100 105 resources: 101 106 requests: 102 107 cpu: 50m
+4
go.mod
··· 13 13 github.com/clerk/clerk-sdk-go/v2 v2.5.0 14 14 github.com/invopop/jsonschema v0.13.0 15 15 github.com/openai/openai-go/v3 v3.21.0 16 + github.com/openclosed-dev/slogan v0.2.0 16 17 github.com/samber/slog-multi v1.5.0 17 18 github.com/sendgrid/rest v2.6.9+incompatible 18 19 github.com/sendgrid/sendgrid-go v3.16.1+incompatible ··· 20 21 ) 21 22 22 23 require ( 24 + code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c // indirect 23 25 github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect 24 26 github.com/bahlo/generic-list-go v0.2.0 // indirect 25 27 github.com/buger/jsonparser v1.1.1 // indirect 26 28 github.com/go-jose/go-jose/v3 v3.0.4 // indirect 29 + github.com/gofrs/uuid v3.3.0+incompatible // indirect 30 + github.com/microsoft/ApplicationInsights-Go v0.4.4 // indirect 27 31 github.com/samber/slog-common v0.19.0 // indirect 28 32 github.com/stretchr/testify v1.11.1 // indirect 29 33 github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
+12
go.sum
··· 1 + code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c h1:5eeuG0BHx1+DHeT3AP+ISKZ2ht1UjGhm581ljqYpVeQ= 2 + code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= 1 3 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo= 2 4 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= 3 5 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= ··· 45 47 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 46 48 github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= 47 49 github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 50 + github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84= 51 + github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 48 52 github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= 49 53 github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 50 54 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= ··· 84 88 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 85 89 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 86 90 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 91 + github.com/microsoft/ApplicationInsights-Go v0.4.4 h1:G4+H9WNs6ygSCe6sUyxRc2U81TI5Es90b2t/MwX5KqY= 92 + github.com/microsoft/ApplicationInsights-Go v0.4.4/go.mod h1:fKRUseBqkw6bDiXTs3ESTiU/4YTIHsQS4W3fP2ieF4U= 87 93 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 88 94 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 89 95 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= ··· 98 104 github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= 99 105 github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= 100 106 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 107 + github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 101 108 github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 102 109 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 103 110 github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= 104 111 github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 105 112 github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 113 + github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 106 114 github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 107 115 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 108 116 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= ··· 111 119 github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= 112 120 github.com/openai/openai-go/v3 v3.21.0 h1:3GpIR/W4q/v1uUOVuK3zYtQiF3DnRrZag/sxbtvEdtc= 113 121 github.com/openai/openai-go/v3 v3.21.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= 122 + github.com/openclosed-dev/slogan v0.2.0 h1:Nh1z0IJ366ADFqu5pZY7SdMcYeONaeCx2J5Od9xHSfs= 123 + github.com/openclosed-dev/slogan v0.2.0/go.mod h1:1c/tiM++o7TnyP1WNnbnMecG2Yjs7kGmkXmVj8WzElg= 114 124 github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= 115 125 github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= 116 126 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= ··· 143 153 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 144 154 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 145 155 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 156 + github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0= 146 157 github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 147 158 github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 148 159 github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= ··· 248 259 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 249 260 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 250 261 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 262 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 251 263 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 252 264 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 253 265 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=