Skip to content

Commit d930d8e

Browse files
authored
Merge pull request #106 from shelltime/feat/ccusage-collection-service
feat(ccusage): add CCUsage collection service for usage tracking
2 parents eaf7ea2 + 13015d8 commit d930d8e

6 files changed

Lines changed: 639 additions & 60 deletions

File tree

cmd/daemon/main.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,17 @@ func main() {
9595

9696
go daemon.SocketTopicProccessor(msg)
9797

98+
// Start CCUsage service if enabled
99+
if cfg.CCUsage != nil && cfg.CCUsage.Enabled != nil && *cfg.CCUsage.Enabled {
100+
ccUsageService := model.NewCCUsageService(cfg)
101+
if err := ccUsageService.Start(ctx); err != nil {
102+
slog.Error("Failed to start CCUsage service", slog.Any("err", err))
103+
} else {
104+
slog.Info("CCUsage service started")
105+
defer ccUsageService.Stop()
106+
}
107+
}
108+
98109
// Create processor instance
99110
processor := daemon.NewSocketHandler(daemonConfig, pubsub)
100111

model/api.base.go

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ type HTTPRequestOptions[T any, R any] struct {
2828
Timeout time.Duration // Optional, defaults to 10 seconds
2929
}
3030

31-
// SendHTTPRequest is a generic HTTP request function that sends data and unmarshals the response
31+
// SendHTTPRequest is a legacy HTTP request function that uses msgpack encoding
32+
// Deprecated: Use SendHTTPRequestJSON for new implementations
3233
func SendHTTPRequest[T any, R any](opts HTTPRequestOptions[T, R]) error {
3334
ctx, span := modelTracer.Start(opts.Context, "http.send")
3435
defer span.End()
@@ -118,3 +119,162 @@ func SendHTTPRequest[T any, R any](opts HTTPRequestOptions[T, R]) error {
118119

119120
return nil
120121
}
122+
123+
// SendHTTPRequestJSON is a generic HTTP request function that sends JSON data and unmarshals the response
124+
func SendHTTPRequestJSON[T any, R any](opts HTTPRequestOptions[T, R]) error {
125+
ctx, span := modelTracer.Start(opts.Context, "http.send.json")
126+
defer span.End()
127+
128+
jsonData, err := json.Marshal(opts.Payload)
129+
if err != nil {
130+
logrus.Errorln(err)
131+
return err
132+
}
133+
134+
timeout := time.Second * 10
135+
if opts.Timeout > 0 {
136+
timeout = opts.Timeout
137+
}
138+
139+
client := &http.Client{
140+
Timeout: timeout,
141+
Transport: otelhttp.NewTransport(http.DefaultTransport),
142+
}
143+
144+
req, err := http.NewRequestWithContext(ctx, opts.Method, opts.Endpoint.APIEndpoint+opts.Path, bytes.NewBuffer(jsonData))
145+
if err != nil {
146+
logrus.Errorln(err)
147+
return err
148+
}
149+
150+
contentType := "application/json"
151+
if opts.ContentType != "" {
152+
contentType = opts.ContentType
153+
}
154+
155+
req.Header.Set("Content-Type", contentType)
156+
req.Header.Set("User-Agent", fmt.Sprintf("shelltimeCLI@%s", commitID))
157+
req.Header.Set("Authorization", "CLI "+opts.Endpoint.Token)
158+
159+
logrus.Traceln("http: ", req.URL.String())
160+
161+
resp, err := client.Do(req)
162+
if err != nil {
163+
logrus.Errorln(err)
164+
return err
165+
}
166+
defer resp.Body.Close()
167+
168+
logrus.Traceln("http: ", resp.Status)
169+
170+
if resp.StatusCode == http.StatusNoContent {
171+
return nil
172+
}
173+
174+
buf, err := io.ReadAll(resp.Body)
175+
if err != nil {
176+
logrus.Errorln(err)
177+
return err
178+
}
179+
180+
if resp.StatusCode != http.StatusOK {
181+
var msg errorResponse
182+
err = json.Unmarshal(buf, &msg)
183+
if err != nil {
184+
logrus.Errorln("Failed to parse error response:", err)
185+
return fmt.Errorf("HTTP error: %d", resp.StatusCode)
186+
}
187+
logrus.Errorln("Error response:", msg.ErrorMessage)
188+
return errors.New(msg.ErrorMessage)
189+
}
190+
191+
// Only try to unmarshal if we have a response struct
192+
if opts.Response != nil {
193+
err = json.Unmarshal(buf, opts.Response)
194+
if err != nil {
195+
logrus.Errorln("Failed to unmarshal JSON response:", err)
196+
return err
197+
}
198+
}
199+
200+
return nil
201+
}
202+
203+
// GraphQLResponse is a generic wrapper for GraphQL responses
204+
type GraphQLResponse[T any] struct {
205+
Data T `json:"data"`
206+
Errors []GraphQLError `json:"errors,omitempty"`
207+
}
208+
209+
// GraphQLError represents a GraphQL error
210+
type GraphQLError struct {
211+
Message string `json:"message"`
212+
Extensions map[string]interface{} `json:"extensions,omitempty"`
213+
Path []interface{} `json:"path,omitempty"`
214+
}
215+
216+
// GraphQLRequestOptions contains options for GraphQL requests
217+
type GraphQLRequestOptions[R any] struct {
218+
Context context.Context
219+
Endpoint Endpoint
220+
Query string
221+
Variables map[string]interface{}
222+
Response *R
223+
Timeout time.Duration // Optional, defaults to 30 seconds
224+
}
225+
226+
// SendGraphQLRequest sends a GraphQL request and unmarshals the response
227+
func SendGraphQLRequest[R any](opts GraphQLRequestOptions[R]) error {
228+
ctx, span := modelTracer.Start(opts.Context, "graphql.send")
229+
defer span.End()
230+
231+
// Build GraphQL payload
232+
payload := map[string]interface{}{
233+
"query": opts.Query,
234+
}
235+
if opts.Variables != nil {
236+
payload["variables"] = opts.Variables
237+
}
238+
239+
// Build GraphQL endpoint path
240+
graphQLPath := "/api/v2/graphql"
241+
242+
// Set default timeout
243+
timeout := time.Second * 30
244+
if opts.Timeout > 0 {
245+
timeout = opts.Timeout
246+
}
247+
248+
// Use the new JSON HTTP request function
249+
err := SendHTTPRequestJSON(HTTPRequestOptions[map[string]interface{}, R]{
250+
Context: ctx,
251+
Endpoint: opts.Endpoint,
252+
Method: http.MethodPost,
253+
Path: graphQLPath,
254+
Payload: payload,
255+
Response: opts.Response,
256+
Timeout: timeout,
257+
})
258+
259+
if err != nil {
260+
// The error is already formatted by SendHTTPRequestJSON
261+
return err
262+
}
263+
264+
// Check for GraphQL errors in the response if we have a response
265+
if opts.Response != nil {
266+
// Marshal response back to check for errors
267+
respBytes, err := json.Marshal(opts.Response)
268+
if err == nil {
269+
var errorCheck struct {
270+
Errors []GraphQLError `json:"errors,omitempty"`
271+
}
272+
if err := json.Unmarshal(respBytes, &errorCheck); err == nil && len(errorCheck.Errors) > 0 {
273+
// Return the first error message if there are GraphQL errors
274+
return fmt.Errorf("GraphQL error: %s", errorCheck.Errors[0].Message)
275+
}
276+
}
277+
}
278+
279+
return nil
280+
}

0 commit comments

Comments
 (0)