Skip to content

Commit 5e93bb2

Browse files
authored
chore: write http requests to telemetry logs (#1108)
This PR updates the `Emit` method to write HTTP related logs to our `telemetry_logs` table.
1 parent 797e25a commit 5e93bb2

File tree

4 files changed

+376
-0
lines changed

4 files changed

+376
-0
lines changed

.changeset/poor-breads-sneeze.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package telemetry_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/google/uuid"
9+
"github.com/speakeasy-api/gram/server/internal/telemetry"
10+
"github.com/speakeasy-api/gram/server/internal/telemetry/repo"
11+
"github.com/speakeasy-api/gram/server/internal/testenv"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestToolCallLogger_EmitCreatesHTTPAndTelemetryLogs(t *testing.T) {
16+
t.Parallel()
17+
18+
ctx := context.Background()
19+
logger := testenv.NewLogger(t)
20+
21+
chConn, err := infra.NewClickhouseClient(t)
22+
require.NoError(t, err)
23+
24+
tracerProvider := testenv.NewTracerProvider(t)
25+
26+
chClient := repo.New(logger, tracerProvider, chConn, func(context.Context, string) (bool, error) {
27+
return true, nil
28+
})
29+
30+
// Create test data
31+
projectID := uuid.New().String()
32+
deploymentID := uuid.New().String()
33+
toolID := uuid.New().String()
34+
organizationID := uuid.New().String()
35+
toolURN := "tools:http:test-source:test-tool"
36+
toolName := "test-tool"
37+
38+
39+
// Create a tool call logger
40+
toolCallLogger, err := telemetry.NewToolCallLogger(
41+
ctx,
42+
chClient,
43+
organizationID,
44+
telemetry.ToolInfo{
45+
ID: toolID,
46+
Urn: toolURN,
47+
Name: toolName,
48+
ProjectID: projectID,
49+
DeploymentID: deploymentID,
50+
OrganizationID: organizationID,
51+
},
52+
toolName,
53+
repo.ToolTypeHTTP,
54+
)
55+
require.NoError(t, err)
56+
require.True(t, toolCallLogger.Enabled())
57+
58+
// Record HTTP request details
59+
toolCallLogger.RecordHTTPMethod("POST")
60+
toolCallLogger.RecordHTTPRoute("/api/test")
61+
toolCallLogger.RecordHTTPServerURL("https://example.com")
62+
toolCallLogger.RecordStatusCode(200)
63+
toolCallLogger.RecordDurationMs(123.45)
64+
toolCallLogger.RecordUserAgent("test-client/1.0")
65+
toolCallLogger.RecordRequestHeaders(map[string]string{"Authorization": "Bearer token"}, true)
66+
toolCallLogger.RecordResponseHeaders(map[string]string{"Content-Type": "application/json"})
67+
toolCallLogger.RecordRequestBodyBytes(100)
68+
toolCallLogger.RecordResponseBodyBytes(150)
69+
70+
now := time.Now().UTC()
71+
72+
// Emit the logs (writes to both http_requests_raw and telemetry_logs)
73+
toolCallLogger.Emit(ctx, logger)
74+
75+
// Wait for async writes to complete (ClickHouse eventual consistency)
76+
var logs []repo.TelemetryLog
77+
require.Eventually(t, func() bool {
78+
var err error
79+
logs, err = chClient.ListTelemetryLogs(ctx, repo.ListTelemetryLogsParams{
80+
GramProjectID: projectID,
81+
TimeStart: now.Add(-1 * time.Minute).UnixNano(),
82+
TimeEnd: now.Add(1 * time.Minute).UnixNano(),
83+
GramURN: toolURN,
84+
SortOrder: "desc",
85+
Cursor: "",
86+
Limit: 10,
87+
})
88+
return err == nil && len(logs) == 1
89+
}, 2*time.Second, 50*time.Millisecond, "Expected 1 log in telemetry_logs table")
90+
91+
// Verify the inserted log
92+
log := logs[0]
93+
require.Equal(t, projectID, log.GramProjectID)
94+
require.Equal(t, deploymentID, *log.GramDeploymentID)
95+
require.Nil(t, log.GramFunctionID)
96+
require.Equal(t, toolURN, log.GramURN)
97+
require.Equal(t, "gram-server", log.ServiceName)
98+
require.Equal(t, "INFO", *log.SeverityText)
99+
require.Equal(t, "POST", *log.HTTPRequestMethod)
100+
require.Equal(t, int32(200), *log.HTTPResponseStatusCode)
101+
require.Equal(t, "/api/test", *log.HTTPRoute)
102+
require.Equal(t, "https://example.com", *log.HTTPServerURL)
103+
require.Contains(t, log.Body, "POST /api/test -> 200")
104+
require.Contains(t, log.Body, "123.45")
105+
106+
// Verify headers are included in attributes
107+
require.Contains(t, log.Attributes, "headers")
108+
require.Contains(t, log.Attributes, "Authorization")
109+
require.Contains(t, log.Attributes, "Bearer") // Redacted token
110+
require.Contains(t, log.Attributes, "Content-Type")
111+
require.Contains(t, log.Attributes, "application\\/json") // JSON escapes forward slashes
112+
}
113+
114+
func TestToolCallLogger_404ErrorLogsWithWarnSeverity(t *testing.T) {
115+
t.Parallel()
116+
117+
ctx := context.Background()
118+
logger := testenv.NewLogger(t)
119+
120+
chConn, err := infra.NewClickhouseClient(t)
121+
require.NoError(t, err)
122+
123+
tracerProvider := testenv.NewTracerProvider(t)
124+
125+
chClient := repo.New(logger, tracerProvider, chConn, func(context.Context, string) (bool, error) {
126+
return true, nil
127+
})
128+
129+
// Create test data
130+
toolID := uuid.New().String()
131+
projectID := uuid.New().String()
132+
deploymentID := uuid.New().String()
133+
organizationID := uuid.New().String()
134+
toolURN := "tools:http:test:warn-severity"
135+
toolName := "test-tool"
136+
137+
// Create a tool call logger
138+
toolCallLogger, err := telemetry.NewToolCallLogger(
139+
ctx,
140+
chClient,
141+
organizationID,
142+
telemetry.ToolInfo{
143+
ID: toolID,
144+
Urn: toolURN,
145+
Name: toolName,
146+
ProjectID: projectID,
147+
DeploymentID: deploymentID,
148+
OrganizationID: organizationID,
149+
},
150+
toolName,
151+
repo.ToolTypeHTTP,
152+
)
153+
require.NoError(t, err)
154+
155+
// Record 404 error
156+
toolCallLogger.RecordHTTPMethod("GET")
157+
toolCallLogger.RecordHTTPRoute("/users")
158+
toolCallLogger.RecordHTTPServerURL("https://api.example.com")
159+
toolCallLogger.RecordStatusCode(404)
160+
toolCallLogger.RecordDurationMs(50.25)
161+
toolCallLogger.RecordUserAgent("test-client/1.0")
162+
toolCallLogger.RecordRequestHeaders(map[string]string{"Authorization": "Bearer token"}, true)
163+
toolCallLogger.RecordResponseHeaders(map[string]string{"Content-Type": "application/json"})
164+
toolCallLogger.RecordResponseBodyBytes(150)
165+
166+
now := time.Now().UTC()
167+
168+
// Emit the logs
169+
toolCallLogger.Emit(ctx, logger)
170+
171+
// Wait for async write (ClickHouse eventual consistency)
172+
var logs []repo.TelemetryLog
173+
require.Eventually(t, func() bool {
174+
var err error
175+
logs, err = chClient.ListTelemetryLogs(ctx, repo.ListTelemetryLogsParams{
176+
GramProjectID: projectID,
177+
TimeStart: now.Add(-1 * time.Minute).UnixNano(),
178+
TimeEnd: now.Add(1 * time.Minute).UnixNano(),
179+
GramURN: toolURN,
180+
SortOrder: "desc",
181+
Cursor: "",
182+
Limit: 10,
183+
})
184+
return err == nil && len(logs) == 1
185+
}, 2*time.Second, 50*time.Millisecond, "Expected 1 log in telemetry_logs table")
186+
187+
log := logs[0]
188+
// Verify 404 was converted to WARN severity
189+
require.NotNil(t, log.SeverityText)
190+
require.Equal(t, "WARN", *log.SeverityText)
191+
require.Equal(t, "GET", *log.HTTPRequestMethod)
192+
require.Equal(t, int32(404), *log.HTTPResponseStatusCode)
193+
require.Contains(t, log.Body, "404")
194+
require.Contains(t, log.Body, "50.25")
195+
196+
// Verify headers are included in attributes
197+
require.Contains(t, log.Attributes, "headers")
198+
require.Contains(t, log.Attributes, "Authorization") // Request header key
199+
require.Contains(t, log.Attributes, "Bearer") // Redacted request header value
200+
require.Contains(t, log.Attributes, "Content-Type") // Response header key
201+
require.Contains(t, log.Attributes, "application\\/json") // Response header value (JSON escapes slashes)
202+
}

server/internal/telemetry/stub.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,22 @@ func (n *StubToolMetricsClient) ListTelemetryLogs(_ context.Context, _ repo.List
2020
return nil, nil
2121
}
2222

23+
func (n *StubToolMetricsClient) ListTraces(_ context.Context, _ repo.ListTracesParams) ([]repo.TraceSummary, error) {
24+
return nil, nil
25+
}
26+
27+
func (n *StubToolMetricsClient) ListLogsForTrace(_ context.Context, _ repo.ListLogsForTraceParams) ([]repo.TelemetryLog, error) {
28+
return nil, nil
29+
}
30+
2331
func (n *StubToolMetricsClient) LogHTTPRequest(_ context.Context, _ repo.ToolHTTPRequest) error {
2432
return nil
2533
}
2634

35+
func (n *StubToolMetricsClient) InsertTelemetryLog(_ context.Context, _ repo.InsertTelemetryLogParams) error {
36+
return nil
37+
}
38+
2739
func (n *StubToolMetricsClient) ShouldLog(_ context.Context, _ string) (bool, error) {
2840
return true, nil
2941
}

0 commit comments

Comments
 (0)