@@ -17,6 +17,21 @@ import (
1717 "github.com/bwmarrin/discordgo"
1818 "github.com/google/uuid"
1919 "github.com/sudomateo/yeetcode/internal/leetcode"
20+ "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
21+ "go.opentelemetry.io/otel"
22+ "go.opentelemetry.io/otel/attribute"
23+ "go.opentelemetry.io/otel/codes"
24+ "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
25+ "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
26+ "go.opentelemetry.io/otel/sdk/resource"
27+ sdktrace "go.opentelemetry.io/otel/sdk/trace"
28+ semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
29+ "go.opentelemetry.io/otel/trace"
30+ )
31+
32+ var tracer = otel .GetTracerProvider ().Tracer (
33+ "github.com/sudomateo/yeetcode" ,
34+ trace .WithSchemaURL (semconv .SchemaURL ),
2035)
2136
2237func main () {
@@ -29,6 +44,43 @@ func main() {
2944}
3045
3146func run (ctx context.Context , logger * slog.Logger ) error {
47+ var exporter sdktrace.SpanExporter
48+
49+ axiomApiToken := os .Getenv ("AXIOM_API_TOKEN" )
50+ if axiomApiToken == "" {
51+ stdoutExp , err := stdouttrace .New ()
52+ if err != nil {
53+ return fmt .Errorf ("failed initializing stdout exporter: %w" , err )
54+ }
55+ exporter = stdoutExp
56+ } else {
57+ httpExp , err := otlptracehttp .New (ctx ,
58+ otlptracehttp .WithEndpoint ("api.axiom.co" ),
59+ otlptracehttp .WithHeaders (map [string ]string {
60+ "Authorization" : fmt .Sprintf ("Bearer %s" , axiomApiToken ),
61+ "X-AXIOM-DATASET" : "yeetcode" ,
62+ }),
63+ )
64+ if err != nil {
65+ return fmt .Errorf ("failed initializing trace exporter: %w" , err )
66+ }
67+ exporter = httpExp
68+ }
69+
70+ tracerProvider := sdktrace .NewTracerProvider (
71+ sdktrace .WithBatcher (exporter ),
72+ sdktrace .WithResource (resource .NewWithAttributes (
73+ semconv .SchemaURL ,
74+ semconv .ServiceName ("yeetcode" ),
75+ )),
76+ )
77+
78+ defer func () {
79+ _ = tracerProvider .Shutdown (ctx )
80+ }()
81+
82+ otel .SetTracerProvider (tracerProvider )
83+
3284 discordToken := os .Getenv ("DISCORD_TOKEN" )
3385 if discordToken == "" {
3486 return errors .New ("DISCORD_TOKEN must be provided" )
@@ -53,35 +105,37 @@ func run(ctx context.Context, logger *slog.Logger) error {
53105
54106 mux := http .NewServeMux ()
55107
56- mux .HandleFunc ("POST /" , func (w http.ResponseWriter , r * http.Request ) {
108+ handleFunc := func (pattern string , handlerFunc func (http.ResponseWriter , * http.Request )) {
109+ handler := otelhttp .WithRouteTag (pattern , http .HandlerFunc (handlerFunc ))
110+ mux .Handle (pattern , handler )
111+ }
112+
113+ handleFunc ("POST /" , func (w http.ResponseWriter , r * http.Request ) {
114+ ctx , span := tracer .Start (r .Context (), "interaction" )
115+ defer span .End ()
116+
57117 requestID , err := uuid .NewRandom ()
58118 if err != nil {
59- logger . Error ( "failed generating request id" )
119+ span . RecordError ( err )
60120 requestID = uuid .UUID ([16 ]byte {})
61121 }
62122
63- logger := logger .With (
64- "request.path" , r .URL .Path ,
65- "request.id" , requestID ,
123+ span .SetAttributes (
124+ attribute .String ("request.id" , requestID .String ()),
66125 )
67126
68- logger .Info ("received request" )
69-
70- requestReceivedAt := time .Now ()
71- defer func () {
72- logger .Info ("finished handling request" , "duration_ms" , time .Since (requestReceivedAt ).Milliseconds ())
73- }()
74-
75127 if ! discordgo .VerifyInteraction (r , publicKeyBytes ) {
76- logger .Error ("failed verifying interaction" )
128+ span .RecordError (err )
129+ span .SetStatus (codes .Error , "failed verifying interaction" )
77130 w .WriteHeader (http .StatusBadRequest )
78131 return
79132 }
80133
81134 defer r .Body .Close ()
82135 var interaction discordgo.Interaction
83136 if err := json .NewDecoder (r .Body ).Decode (& interaction ); err != nil {
84- logger .Error ("invalid interaction payload" , "error" , err )
137+ span .RecordError (err )
138+ span .SetStatus (codes .Error , "invalid interaction payload" )
85139 w .WriteHeader (http .StatusBadRequest )
86140 return
87141 }
@@ -93,7 +147,8 @@ func run(ctx context.Context, logger *slog.Logger) error {
93147 }
94148
95149 if err := json .NewEncoder (w ).Encode (resp ); err != nil {
96- logger .Error ("failed sending ping response" , "error" , err )
150+ span .RecordError (err )
151+ span .SetStatus (codes .Error , "failed sending ping response" )
97152 w .WriteHeader (http .StatusInternalServerError )
98153 return
99154 }
@@ -102,38 +157,17 @@ func run(ctx context.Context, logger *slog.Logger) error {
102157 }
103158
104159 if interaction .Type != discordgo .InteractionApplicationCommand {
105- logger .Error ("unsupported interaction type" , "type" , interaction .Type )
160+ span .RecordError (err )
161+ span .SetStatus (codes .Error , "unsupported interaction type" )
106162 w .WriteHeader (http .StatusBadRequest )
107163 return
108164 }
109165
110166 applicationCommandData := interaction .ApplicationCommandData ()
111-
112- var difficultyOpt string
113-
114- for _ , v := range applicationCommandData .Options {
115- if v .Name == "difficulty" {
116- difficultyOpt = strings .ToUpper (v .StringValue ())
117- break
118- }
119- }
120-
121- var difficulty leetcode.Difficulty
122-
123- switch leetcode .Difficulty (difficultyOpt ) {
124- case leetcode .DifficultyEasy :
125- difficulty = leetcode .DifficultyEasy
126- case leetcode .DifficultyMedium :
127- difficulty = leetcode .DifficultyMedium
128- case leetcode .DifficultyHard :
129- difficulty = leetcode .DifficultyHard
130- default :
131- difficulty = leetcode .RandomDifficulty ()
132- }
133-
134- lcResp , err := leetcodeClient .RandomQuestion (difficulty )
167+ lcResp , err := fetchLeetCodeQuestion (ctx , & leetcodeClient , & applicationCommandData )
135168 if err != nil {
136- logger .Error ("failed to retrieve leetcode question" , "error" , err )
169+ span .RecordError (err )
170+ span .SetStatus (codes .Error , "failed to retreive leetcode question" )
137171 w .WriteHeader (http .StatusInternalServerError )
138172 return
139173 }
@@ -146,12 +180,15 @@ func run(ctx context.Context, logger *slog.Logger) error {
146180 }
147181
148182 if err := discordClient .InteractionRespond (& interaction , & interactionResp ); err != nil {
149- logger .Error ("failed responding to interaction" )
183+ span .RecordError (err )
184+ span .SetStatus (codes .Error , "failed responding to interaction" )
150185 w .WriteHeader (http .StatusInternalServerError )
151186 return
152187 }
153188
154- logger .Info ("responded to interaction" , "leetcode" , lcResp .Data .RandomQuestion .TitleSlug , "difficulty" , difficulty )
189+ span .SetAttributes (
190+ attribute .String ("leetcode.title_slug" , lcResp .Data .RandomQuestion .TitleSlug ),
191+ )
155192
156193 w .WriteHeader (http .StatusOK )
157194 return
@@ -188,3 +225,35 @@ func run(ctx context.Context, logger *slog.Logger) error {
188225
189226 return nil
190227}
228+
229+ // This is just here to have a parent/child span relationship for Axiom.
230+ func fetchLeetCodeQuestion (ctx context.Context , leetcodeClient * leetcode.Client , applicationCommandData * discordgo.ApplicationCommandInteractionData ) (leetcode.RandomQuestionResponse , error ) {
231+ ctx , span := tracer .Start (ctx , "fetchLeetCodeQuestion" )
232+ defer span .End ()
233+
234+ var difficultyOpt string
235+
236+ for _ , v := range applicationCommandData .Options {
237+ if v .Name == "difficulty" {
238+ difficultyOpt = strings .ToUpper (v .StringValue ())
239+ break
240+ }
241+ }
242+
243+ var difficulty leetcode.Difficulty
244+
245+ switch leetcode .Difficulty (difficultyOpt ) {
246+ case leetcode .DifficultyEasy :
247+ difficulty = leetcode .DifficultyEasy
248+ case leetcode .DifficultyMedium :
249+ difficulty = leetcode .DifficultyMedium
250+ case leetcode .DifficultyHard :
251+ difficulty = leetcode .DifficultyHard
252+ default :
253+ difficulty = leetcode .RandomDifficulty ()
254+ }
255+
256+ span .SetAttributes (attribute .String ("leetcode.difficulty" , string (difficulty )))
257+
258+ return leetcodeClient .RandomQuestion (difficulty )
259+ }
0 commit comments