Skip to content

Commit a96132f

Browse files
Merge pull request #904 from Nikokolas3270/SREP-347
SREP-347 & SREP-4896: factorizing code, fixing some potential race conditions in StreamLogs & making sure 'osdctl rhobs metrics' can work at a given time or with a time range
2 parents 486780d + 5bf35fd commit a96132f

9 files changed

Lines changed: 474 additions & 312 deletions

File tree

cmd/rhobs/cellCmd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func newCmdCell() *cobra.Command {
3939
fmt.Println("Logs RHOBS cell :", logsRhobsFetcher.RhobsCell)
4040
}
4141

42-
if metricsErr != nil && logsErr != nil {
42+
if metricsErr != nil || logsErr != nil {
4343
os.Exit(1)
4444
}
4545
},

cmd/rhobs/logsCmd.go

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,15 @@ func newCmdLogs() *cobra.Command {
3636
var isPrintingTimestamp bool
3737
var printedFields []string
3838

39-
nowTime := time.Now()
40-
defaultStartTime := nowTime.Add(-5 * time.Minute)
41-
4239
cmd := &cobra.Command{
43-
Use: "logs [pod]",
44-
Short: "Fetch logs from RHOBS for a given cluster",
40+
Use: "logs [pod]",
41+
Short: "Fetch logs from RHOBS for a given cluster",
42+
Long: "Fetch logs from RHOBS for a given cluster. " +
43+
"The cluster can be a management cluster (MC) or whatever cluster sending logs to RHOBS; " +
44+
"the command works as if the management cluster ID was passed if given a hosted cluster (HCP) ID. " +
45+
"By default, logs from all the pods in the given namespace are returned but it is possible to specify " +
46+
"a single pod as an argument or filter pods using their labels. Logs themselves can be also filtered " +
47+
"to only keep the ones containing a given regexp (--contain-regex option) or a given log level (--level option).",
4548
Args: cobra.RangeArgs(0, 1),
4649
SilenceErrors: true,
4750
RunE: func(cmd *cobra.Command, args []string) error {
@@ -152,6 +155,9 @@ func newCmdLogs() *cobra.Command {
152155
}
153156
}
154157

158+
nowTime := time.Now()
159+
defaultStartTime := nowTime.Add(-5 * time.Minute)
160+
155161
if cmd.Flags().Changed("since") {
156162
startTime = nowTime.Add(-duration)
157163
} else if !cmd.Flags().Changed("start-time") && !cmd.Flags().Changed("since-time") {
@@ -206,7 +212,7 @@ func newCmdLogs() *cobra.Command {
206212
}
207213

208214
if isFollowing {
209-
err = rhobsFetcher.StreamLogs(lokiExpr, startTime, outputFormat, isPrintingTimestamp, printedFields)
215+
err = rhobsFetcher.StreamLogs(lokiExpr, outputFormat, isPrintingTimestamp, printedFields)
210216
} else {
211217
err = rhobsFetcher.PrintLogs(lokiExpr, startTime, endTime, logsCount, isGoingForward, outputFormat, isPrintingTimestamp, printedFields)
212218
}
@@ -217,7 +223,7 @@ func newCmdLogs() *cobra.Command {
217223
},
218224
}
219225

220-
cmd.Flags().StringVarP(&lokiExpr, "query", "q", "", "Loki query - exclusive with many other flags")
226+
cmd.Flags().StringVarP(&lokiExpr, "query", "q", "", "LogQL expression - exclusive with many other flags")
221227
cmd.Flags().StringVarP(&namespace, "namespace", "n", "default", "Name of the namespace")
222228
cmd.Flags().StringVarP(&labelSelectorStr, "selector", "l", "", "Label selector for filtering pods - exclusive with the pod argument")
223229
cmd.Flags().StringVarP(&containerName, "container", "c", "", "Name of the container - print all containers logs if not specified")
@@ -238,8 +244,8 @@ func newCmdLogs() *cobra.Command {
238244
cmd.MarkFlagsMutuallyExclusive("query", "container")
239245
cmd.MarkFlagsMutuallyExclusive("query", "include-events")
240246

241-
cmd.Flags().TimeVar(&startTime, "start-time", time.Time{}, []string{time.RFC3339}, "Start time for the log query - alternate alias: --since-time (default to 5 minutes ago)")
242-
cmd.Flags().TimeVar(&endTime, "end-time", time.Time{}, []string{time.RFC3339}, "End time for the log query (default to now)")
247+
cmd.Flags().TimeVar(&startTime, "start-time", time.Time{}, []string{time.RFC3339}, "Start time for the logs - alternate alias: --since-time (default to 5 minutes ago)")
248+
cmd.Flags().TimeVar(&endTime, "end-time", time.Time{}, []string{time.RFC3339}, "End time for the logs (default to now)")
243249
cmd.Flags().TimeVar(&startTime, "since-time", time.Time{}, []string{time.RFC3339}, "Same as --start-time")
244250
_ = cmd.Flags().MarkHidden("since-time")
245251
cmd.Flags().DurationVar(&duration, "since", 0, "Only return logs newer than a relative duration (e.g. 1h, 30m) - exclusive with --start-time & --end-time")
@@ -251,8 +257,11 @@ func newCmdLogs() *cobra.Command {
251257
`"backward" returns the most recent & interesting logs first, while "forward" matches the behavior of "kubectl logs" by returning the oldest logs first `+
252258
`(default to "backward" unless --follow is set in which case it is forced to "forward")`)
253259

254-
cmd.Flags().BoolVarP(&isFollowing, "follow", "f", false, "Specify if the logs should be streamed - exclusive with --end-time, --direction, --limit and --no-limit flags")
260+
cmd.Flags().BoolVarP(&isFollowing, "follow", "f", false, "Specify if the logs should be streamed - exclusive with --start-time, --end-time, --since, --direction, --limit and --no-limit flags")
261+
cmd.MarkFlagsMutuallyExclusive("follow", "start-time")
262+
cmd.MarkFlagsMutuallyExclusive("follow", "since-time")
255263
cmd.MarkFlagsMutuallyExclusive("follow", "end-time")
264+
cmd.MarkFlagsMutuallyExclusive("follow", "since")
256265
cmd.MarkFlagsMutuallyExclusive("follow", "direction")
257266

258267
cmd.Flags().IntVar(&logsCount, "limit", 0, "Maximum number of logs to return - allowed range: [1 100000] (default to 10000 unless --follow is set in which case there is no limit)")

cmd/rhobs/mcp_query.go

Lines changed: 0 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@ import (
55
"encoding/json"
66
"fmt"
77
"net/http"
8-
"sort"
9-
"strconv"
108
"time"
119

1210
rhobsclient "github.com/observatorium/api/client"
13-
rhobsparameters "github.com/observatorium/api/client/parameters"
1411
)
1512

1613
type mcpLogEntry struct {
@@ -19,170 +16,6 @@ type mcpLogEntry struct {
1916
Stream map[string]string `json:"stream,omitempty"`
2017
}
2118

22-
func (q *RhobsFetcher) QueryInstantMetrics(ctx context.Context, promExpr string, filterCluster bool) ([]instantMetricResult, error) {
23-
client, err := q.getClient()
24-
if err != nil {
25-
return nil, err
26-
}
27-
28-
promQuery := rhobsparameters.PromqlQuery(promExpr)
29-
queryParams := &rhobsclient.GetInstantQueryParams{Query: &promQuery}
30-
31-
response, err := client.GetInstantQueryWithResponse(ctx, "hcp", queryParams)
32-
if err != nil {
33-
return nil, fmt.Errorf("failed to send request to RHOBS: %v", err)
34-
}
35-
if response.HTTPResponse.StatusCode != http.StatusOK {
36-
return nil, fmt.Errorf("RHOBS query failed with status code: %d - body: %s", response.HTTPResponse.StatusCode, string(response.Body))
37-
}
38-
39-
var formattedResponse getInstantMetricsResponse
40-
if err := json.Unmarshal(response.Body, &formattedResponse); err != nil {
41-
return nil, fmt.Errorf("failed to unmarshal response from RHOBS: %v", err)
42-
}
43-
if formattedResponse.Status != "success" {
44-
return nil, fmt.Errorf("RHOBS query failed with status: %s", formattedResponse.Status)
45-
}
46-
47-
if !filterCluster {
48-
return formattedResponse.Data.Results, nil
49-
}
50-
51-
var filtered []instantMetricResult
52-
for _, result := range formattedResponse.Data.Results {
53-
if q.isManagementCluster {
54-
mcId := result.Metric["_mc_id"]
55-
mcName := result.Metric["mc_name"]
56-
if mcId == q.clusterId || mcName == q.clusterName {
57-
filtered = append(filtered, result)
58-
}
59-
} else {
60-
if result.Metric["_id"] == q.clusterExternalId {
61-
filtered = append(filtered, result)
62-
}
63-
}
64-
}
65-
return filtered, nil
66-
}
67-
68-
func (q *RhobsFetcher) QueryRangeMetrics(ctx context.Context, promExpr, start, end, step string) (json.RawMessage, error) {
69-
client, err := q.getClient()
70-
if err != nil {
71-
return nil, err
72-
}
73-
74-
promQuery := rhobsparameters.PromqlQuery(promExpr)
75-
queryParams := &rhobsclient.GetRangeQueryParams{
76-
Query: &promQuery,
77-
Start: (*rhobsparameters.StartTS)(&start),
78-
End: (*rhobsparameters.EndTS)(&end),
79-
Step: &step,
80-
}
81-
82-
response, err := client.GetRangeQueryWithResponse(ctx, "hcp", queryParams)
83-
if err != nil {
84-
return nil, fmt.Errorf("failed to send request to RHOBS: %v", err)
85-
}
86-
if response.HTTPResponse.StatusCode != http.StatusOK {
87-
return nil, fmt.Errorf("RHOBS query failed with status code: %d - body: %s", response.HTTPResponse.StatusCode, string(response.Body))
88-
}
89-
90-
return response.Body, nil
91-
}
92-
93-
func (q *RhobsFetcher) QueryLogs(ctx context.Context, lokiExpr string, startTime, endTime time.Time, logsCount int) ([]mcpLogEntry, error) {
94-
client, err := q.getClient()
95-
if err != nil {
96-
return nil, err
97-
}
98-
99-
startTimeStamp := startTime.UnixNano()
100-
endTimeStamp := endTime.UnixNano()
101-
logsDir := "backward"
102-
103-
var entries []mcpLogEntry
104-
105-
for logsCount > 0 {
106-
lokiQuery := rhobsparameters.LogqlQuery(lokiExpr)
107-
startTimeStr := strconv.FormatInt(startTimeStamp, 10)
108-
endTimeStr := strconv.FormatInt(endTimeStamp, 10)
109-
110-
limit := float32(500)
111-
if logsCount < 500 {
112-
limit = float32(logsCount)
113-
}
114-
115-
queryParams := &rhobsclient.GetLogRangeQueryParams{
116-
Query: &lokiQuery,
117-
Start: (*rhobsparameters.StartTS)(&startTimeStr),
118-
End: (*rhobsparameters.EndTS)(&endTimeStr),
119-
Direction: &logsDir,
120-
Limit: (*rhobsparameters.Limit)(&limit),
121-
}
122-
123-
response, err := client.GetLogRangeQueryWithResponse(ctx, "hcp", queryParams)
124-
if err != nil {
125-
return nil, fmt.Errorf("failed to send request to RHOBS: %v", err)
126-
}
127-
if response.HTTPResponse.StatusCode != http.StatusOK {
128-
return nil, fmt.Errorf("RHOBS query failed with status code: %d - body: %s", response.HTTPResponse.StatusCode, string(response.Body))
129-
}
130-
131-
var formattedResponse getLogsResponse
132-
if err := json.Unmarshal(response.Body, &formattedResponse); err != nil {
133-
return nil, fmt.Errorf("failed to unmarshal response from RHOBS: %v", err)
134-
}
135-
if formattedResponse.Status != "success" {
136-
return nil, fmt.Errorf("RHOBS query failed with status: %s", formattedResponse.Status)
137-
}
138-
if len(formattedResponse.Data.Results) == 0 {
139-
break
140-
}
141-
142-
var flattenedResults []*logResult
143-
for _, result := range formattedResponse.Data.Results {
144-
for valIdx := range result.Values {
145-
flattenedResults = append(flattenedResults, &logResult{
146-
Stream: result.Stream,
147-
Values: []*[]string{result.Values[valIdx]},
148-
})
149-
}
150-
}
151-
152-
sort.Slice(flattenedResults, func(i, j int) bool {
153-
return flattenedResults[i].getTimeStamp() > flattenedResults[j].getTimeStamp()
154-
})
155-
156-
edgeTimeStamp := flattenedResults[len(flattenedResults)-1].getTimeStamp()
157-
if flattenedResults[0].getTimeStamp() == edgeTimeStamp {
158-
endTimeStamp = edgeTimeStamp - 1
159-
edgeTimeStamp = 0
160-
} else {
161-
endTimeStamp = edgeTimeStamp
162-
}
163-
164-
for _, result := range flattenedResults {
165-
ts := result.getTimeStamp()
166-
if ts != edgeTimeStamp {
167-
entry := mcpLogEntry{
168-
Timestamp: result.getTime(),
169-
Message: result.getMessage(),
170-
}
171-
if result.Stream != nil {
172-
entry.Stream = *result.Stream
173-
}
174-
entries = append(entries, entry)
175-
logsCount--
176-
if logsCount <= 0 {
177-
break
178-
}
179-
}
180-
}
181-
}
182-
183-
return entries, nil
184-
}
185-
18619
func (q *RhobsFetcher) QueryAlerts(ctx context.Context) (json.RawMessage, error) {
18720
client, err := q.getClient()
18821
if err != nil {

cmd/rhobs/mcp_tools.go

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,8 @@ func registerMcpTools(s *mcp.Server) {
4040
"cell": {"type": "string", "description": "RHOBS cell URL (regional Thanos/Loki endpoint)"},
4141
"cluster_id": {"type": "string", "description": "Internal cluster ID"},
4242
"environment": {"type": "string", "description": "OCM environment (production, stage, integration)"},
43-
"results": {"type": "array", "description": "Metric results with labels and values (instant query)"},
44-
"count": {"type": "integer", "description": "Number of results (instant query)"},
45-
"result": {"type": "object", "description": "Range query result (when start/end provided)"}
43+
"results": {"type": "array", "description": "Metric results with labels and values"},
44+
"count": {"type": "integer", "description": "Number of results"}
4645
}
4746
}`),
4847
}, handleMetrics)
@@ -161,25 +160,24 @@ func handleMetrics(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallTool
161160
}
162161

163162
if start != "" && end != "" {
164-
rawResult, err := fetcher.QueryRangeMetrics(ctx, query, start, end, step)
163+
results, err := fetcher.QueryRangeMetrics(ctx, query, NewRawMetricsTimeRange(start, end, step))
165164
if err != nil {
166165
return mcpError("Range metrics query failed: %v", err)
167166
}
168-
var parsed interface{}
169-
if err := json.Unmarshal(rawResult, &parsed); err != nil {
170-
cellInfo["result_raw"] = string(rawResult)
171-
return mcpResultJSON(cellInfo)
172-
}
173-
cellInfo["result"] = parsed
167+
results = filterMetricsResults(fetcher, results, filterCluster)
168+
cellInfo["results"] = results
169+
cellInfo["count"] = len(*results)
174170
return mcpResultJSON(cellInfo)
175171
}
176172

177-
results, err := fetcher.QueryInstantMetrics(ctx, query, filterCluster)
173+
results, err := fetcher.QueryInstantMetrics(ctx, query, time.Time{})
178174
if err != nil {
179175
return mcpError("Metrics query failed: %v", err)
180176
}
177+
results = filterMetricsResults(fetcher, results, filterCluster)
178+
181179
cellInfo["results"] = results
182-
cellInfo["count"] = len(results)
180+
cellInfo["count"] = len(*results)
183181
return mcpResultJSON(cellInfo)
184182
}
185183

@@ -232,7 +230,17 @@ func handleLogs(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolRes
232230
now := time.Now()
233231
startTime := now.Add(-duration)
234232

235-
entries, err := fetcher.QueryLogs(ctx, lokiExpr, startTime, now, limit)
233+
entries := []mcpLogEntry{}
234+
err = fetcher.QueryLogs(ctx, lokiExpr, startTime, now, limit, false, func(result *logResult) {
235+
entry := mcpLogEntry{
236+
Timestamp: result.getTime(),
237+
Message: result.getMessage(),
238+
}
239+
if result.Stream != nil {
240+
entry.Stream = *result.Stream
241+
}
242+
entries = append(entries, entry)
243+
})
236244
if err != nil {
237245
return mcpError("Log query failed: %v", err)
238246
}

0 commit comments

Comments
 (0)