Skip to content

Commit fbae8b0

Browse files
danceratopzmarioevz
authored andcommitted
internal/libhive: Add line numbering for shared client log segments
Add line number tracking to shared client log segments to enable proper highlighting in the UI. Adds a line counting algorithm to convert byte positions to line numbers and enhances related data structures.
1 parent 23993bc commit fbae8b0

File tree

4 files changed

+161
-15
lines changed

4 files changed

+161
-15
lines changed

internal/libhive/api.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,47 @@ func (api *simAPI) startClient(w http.ResponseWriter, r *http.Request) {
500500
return
501501
}
502502

503+
// Check if this is a reference to a shared client
504+
if clientConfig.SharedClientID != "" {
505+
// This is a reference to a shared client - get the shared client info
506+
sharedClient, err := api.tm.GetSharedClient(suiteID, clientConfig.SharedClientID)
507+
if err != nil {
508+
slog.Error("API: shared client not found", "sharedClientId", clientConfig.SharedClientID, "error", err)
509+
serveError(w, err, http.StatusNotFound)
510+
return
511+
}
512+
513+
// Create a reference to the shared client in this test
514+
clientInfo := &ClientInfo{
515+
ID: sharedClient.ID,
516+
IP: sharedClient.IP,
517+
Name: sharedClient.Name,
518+
InstantiatedAt: sharedClient.InstantiatedAt,
519+
LogFile: sharedClient.LogFile,
520+
IsShared: true,
521+
SharedClientID: clientConfig.SharedClientID,
522+
LogPosition: sharedClient.LogPosition,
523+
SuiteID: suiteID, // Make sure this is properly set
524+
}
525+
526+
slog.Debug("Created shared client reference",
527+
"nodeID", clientConfig.SharedClientID,
528+
"name", sharedClient.Name,
529+
"isShared", true,
530+
"logPosition", sharedClient.LogPosition,
531+
"logFile", sharedClient.LogFile)
532+
533+
// Register the node with the test
534+
api.tm.RegisterNode(testID, clientConfig.SharedClientID, clientInfo)
535+
536+
// Return success with the node info
537+
slog.Info("API: shared client registered with test", "suite", suiteID, "test", testID,
538+
"sharedClientId", clientConfig.SharedClientID, "container", sharedClient.ID[:8])
539+
serveJSON(w, &simapi.StartNodeResponse{ID: sharedClient.ID, IP: sharedClient.IP})
540+
return
541+
}
542+
543+
// Normal client startup flow
503544
// Get the client name.
504545
clientDef, err := api.checkClient(&clientConfig)
505546
if err != nil {

internal/libhive/data.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,11 @@ type TestResult struct {
162162

163163
// ClientLogSegment represents a segment of a client log file
164164
type ClientLogSegment struct {
165-
Start int64 `json:"start"` // Starting offset in log file
166-
End int64 `json:"end"` // Ending offset in log file
167-
ClientID string `json:"clientId"` // ID of the client
165+
Start int64 `json:"start"` // Starting byte offset in log file
166+
End int64 `json:"end"` // Ending byte offset in log file
167+
StartLine int `json:"startLine"` // Starting line number
168+
EndLine int `json:"endLine"` // Ending line number
169+
ClientID string `json:"clientId"` // ID of the client
168170
}
169171

170172
type TestLogOffsets struct {
@@ -181,10 +183,10 @@ type ClientInfo struct {
181183
LogFile string `json:"logFile"` //Absolute path to the logfile.
182184

183185
// Fields for shared client support
184-
IsShared bool `json:"isShared"` // Indicates if this client is shared across tests
185-
LogPosition int64 `json:"logPosition"` // Current position in log file for shared clients
186-
SuiteID TestSuiteID `json:"suiteId,omitempty"` // Suite ID for shared clients
187-
SharedClientID string `json:"sharedClientId,omitempty"` // ID of the shared client (if this is a reference)
186+
IsShared bool `json:"isShared"` // Indicates if this client is shared across tests
187+
LogPosition int64 `json:"logPosition"` // Current position in log file for shared clients
188+
SuiteID TestSuiteID `json:"suiteId,omitempty"` // Suite ID for shared clients
189+
SharedClientID string `json:"sharedClientId,omitempty"` // ID of the shared client (if this is a reference)
188190

189191
wait func()
190192
}

internal/libhive/testmanager.go

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -672,11 +672,29 @@ func (manager *TestManager) EndTest(suiteID TestSuiteID, testID TestID, result *
672672
continue
673673
}
674674

675-
// Create log segment for this test
675+
slog.Debug("Processing shared client logs",
676+
"testID", testID,
677+
"nodeID", nodeID,
678+
"clientName", clientInfo.Name,
679+
"startPosition", clientInfo.LogPosition,
680+
"endPosition", currentPosition)
681+
682+
// Count lines in the log segment to determine line numbers
683+
startLine, endLine, err := manager.countLinesInSegment(clientInfo.LogFile, clientInfo.LogPosition, currentPosition)
684+
if err != nil {
685+
slog.Error("could not count lines in client log segment", "err", err)
686+
// If we can't count lines, use 1 to indicate the beginning of the file
687+
startLine = 1
688+
endLine = 1
689+
}
690+
691+
// Create log segment for this test with both byte offsets and line numbers
676692
result.ClientLogs[nodeID] = &ClientLogSegment{
677-
Start: clientInfo.LogPosition,
678-
End: currentPosition,
679-
ClientID: clientInfo.ID,
693+
Start: clientInfo.LogPosition,
694+
End: currentPosition,
695+
StartLine: startLine,
696+
EndLine: endLine,
697+
ClientID: clientInfo.ID,
680698
}
681699

682700
// Extract log segment to a dedicated file for hiveview
@@ -933,6 +951,82 @@ func (manager *TestManager) UnpauseNode(testID TestID, nodeID string) error {
933951
return nil
934952
}
935953

954+
// countLinesInSegment counts the number of lines in a file segment between startByte and endByte.
955+
// Returns the starting and ending line numbers (1-based).
956+
func (manager *TestManager) countLinesInSegment(filePath string, startByte, endByte int64) (int, int, error) {
957+
// Ensure we have the full path to the log file
958+
fullPath := filePath
959+
if !filepath.IsAbs(fullPath) {
960+
fullPath = filepath.Join(manager.config.LogDir, filePath)
961+
}
962+
slog.Debug("Opening log file", "path", fullPath)
963+
964+
// Open the log file
965+
file, err := os.Open(fullPath)
966+
if err != nil {
967+
return 1, 1, err
968+
}
969+
defer file.Close()
970+
971+
// Count lines up to the start position to get the starting line number
972+
startLine := 1
973+
if startByte > 0 {
974+
buffer := make([]byte, startByte)
975+
_, err = file.Read(buffer)
976+
if err != nil && err != io.EOF {
977+
return 1, 1, err
978+
}
979+
980+
// Count newlines in the buffer
981+
for _, b := range buffer {
982+
if b == '\n' {
983+
startLine++
984+
}
985+
}
986+
}
987+
988+
// Now count lines in the segment to determine the ending line number
989+
bufferSize := endByte - startByte
990+
if bufferSize <= 0 {
991+
return startLine, startLine, nil
992+
}
993+
994+
// Seek to the start position
995+
_, err = file.Seek(startByte, 0)
996+
if err != nil {
997+
return startLine, startLine, err
998+
}
999+
1000+
// Read the segment
1001+
buffer := make([]byte, bufferSize)
1002+
_, err = file.Read(buffer)
1003+
if err != nil && err != io.EOF {
1004+
return startLine, startLine, err
1005+
}
1006+
1007+
// Count newlines in the segment
1008+
endLine := startLine
1009+
for _, b := range buffer {
1010+
if b == '\n' {
1011+
endLine++
1012+
}
1013+
}
1014+
1015+
// Adjust endLine to fix the off-by-1 issue (the actual line with content rather than the next line)
1016+
if endLine > startLine {
1017+
endLine--
1018+
}
1019+
1020+
slog.Debug("Counted lines in segment",
1021+
"file", filePath,
1022+
"startByte", startByte,
1023+
"endByte", endByte,
1024+
"startLine", startLine,
1025+
"endLine", endLine)
1026+
1027+
return startLine, endLine, nil
1028+
}
1029+
9361030
// writeSuiteFile writes the simulation result to the log directory.
9371031
// List of build arguments to exclude from result JSON for security/privacy
9381032
var excludedBuildArgs = map[string]bool{

internal/simapi/simapi.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ type TestRequest struct {
1111

1212
// NodeConfig contains the launch parameters for a client container.
1313
type NodeConfig struct {
14-
Client string `json:"client"`
15-
Networks []string `json:"networks"`
16-
Environment map[string]string `json:"environment"`
17-
IsShared bool `json:"isShared,omitempty"` // Whether this client is shared across tests
14+
Client string `json:"client"`
15+
Networks []string `json:"networks"`
16+
Environment map[string]string `json:"environment"`
17+
IsShared bool `json:"isShared,omitempty"` // Whether this client is shared across tests
18+
SharedClientID string `json:"sharedClientId,omitempty"` // If set, this is a reference to an existing shared client
1819
}
1920

2021
// StartNodeResponse is returned by the client startup endpoint.
@@ -29,6 +30,14 @@ type NodeResponse struct {
2930
Name string `json:"name"`
3031
}
3132

33+
// NodeInfo contains information about a client node to register with a test.
34+
type NodeInfo struct {
35+
ID string `json:"id"` // Container ID
36+
Name string `json:"name"` // Client name/type
37+
IsShared bool `json:"isShared"` // Whether this is a shared client
38+
SharedClientID string `json:"sharedClientId,omitempty"` // ID of the shared client in the suite
39+
}
40+
3241
type ExecRequest struct {
3342
Command []string `json:"command"`
3443
}

0 commit comments

Comments
 (0)