I recently interviewed BrowserStack for the Backend Engineer role and I was asked to implement a remote log streamer service in the machine coding round.
Assignment: Implement a server-side program to monitor a log file and capable of streaming updates that happen in it.
- The service will run on the same machine as the log file.
- Build a web client that prints the updates in the file as and when they happen and NOT upon page refresh.
- The page should be loaded once and it should keep getting updated in real-time.
- The user sees the last 10 lines in the file when he lands on the page.
Implementation: (Golang)
// main.go
package main
import (
"bytes"
"fmt"
"log"
"net/http"
"os"
"strings"
"sync"
"time"
"github.com/fsnotify/fsnotify"
"github.com/gorilla/websocket"
)
const (
linesToKeep = 10
chunkSize = int64(1024)
clientBufSize = 256
)
type Ring struct {
mu sync.RWMutex
buf []string
size int
index int
full bool
}
func NewRing(n int) *Ring {
return &Ring{buf: make([]string, n), size: n}
}
func (r *Ring) Push(line string) {
r.mu.Lock()
r.buf[r.index] = line
r.index = (r.index + 1) % r.size
if r.index == 0 {
r.full = true
}
r.mu.Unlock()
}
func (r *Ring) Snapshot() []string {
if !r.full {
return append([]string{}, r.buf[:r.index]...)
}
res := make([]string, 0, r.size)
res = append(res, r.buf[r.index:]...)
res = append(res, r.buf[:r.index]...)
return res
}
func ReadLastN(filepath string, linesToRead int, chunkSize int64) ([]string, error) {
f, err := os.Open(filepath)
if err != nil {
return nil, err
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
return nil, err
}
size := stat.Size()
if size == 0 {
return nil, nil
}
var offset = size
var data []byte
for offset > 0 && bytes.Count(data, []byte{'\n'}) <= linesToRead {
if offset < chunkSize {
chunkSize = offset
}
offset -= chunkSize
buf := make([]byte, chunkSize)
n, _ := f.ReadAt(buf, offset)
if n > 0 {
data = append(buf[:n], data...)
}
}
lines := bytes.Split(data, []byte{'\n'})
res := make([]string, 0, linesToRead)
for i := len(lines) - 1; i >= 0 && len(res) < linesToRead; i-- {
line := strings.TrimSpace(string(lines[i]))
if line == "" {
continue
}
res = append(res, line)
}
// reverse
for i, j := 0, len(res)-1; i < j; i, j = i+1, j-1 {
res[i], res[j] = res[j], res[i]
}
return res, nil
}
type Client struct {
conn *websocket.Conn
out chan string
unregister func(*Client)
once sync.Once
}
func (c *Client) Close() {
c.once.Do(func() {
c.unregister(c)
})
}
var (
registerCh = make(chan *Client)
unregisterCh = make(chan *Client)
broadcastCh = make(chan string, 1024)
)
func broadcaster(r *Ring) {
clients := make(map[*Client]bool)
for {
select {
case line := <-broadcastCh:
for c := range clients {
select {
case c.out <- line:
default:
go func(cl *Client) { cl.Close() }(c)
}
}
case c := <-registerCh:
snap := r.Snapshot()
for _, s := range snap {
select {
case c.out <- s:
default:
go func(cl *Client) { cl.Close() }(c)
}
}
clients[c] = true
case c := <-unregisterCh:
if _, ok := clients[c]; ok {
delete(clients, c)
close(c.out)
}
}
}
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
func serveWS() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
wsConn, err := upgrader.Upgrade(w, req, nil)
if err != nil {
http.Error(w, "upgrade failed", http.StatusBadRequest)
return
}
client := &Client{
conn: wsConn,
out: make(chan string, clientBufSize),
unregister: func(c *Client) {
unregisterCh <- c
},
}
go func(c *Client) {
for {
msg, _ := <-c.out
if err := c.conn.WriteMessage(websocket.TextMessage, []byte(msg)); err != nil {
c.Close()
return
}
}
}(client)
// register with broadcaster (broadcaster will send snapshot then add)
registerCh <- client
}
}
func startWatcher(filepath string, ring *Ring) {
stat, err := os.Stat(filepath)
var offset int64 = 0
if err == nil {
offset = stat.Size()
}
partial := []byte{}
// readNew reads appended bytes since offset, extracts complete lines,
// pushes them to ring and broadcastCh.
readNew := func() {
f, err := os.Open(filepath)
if err != nil {
return
}
defer f.Close()
st, err := f.Stat()
if err != nil {
return
}
newSize := st.Size()
if newSize <= offset {
return
}
n64 := newSize - offset
buf := make([]byte, int(n64))
nread, _ := f.ReadAt(buf, offset)
offset += int64(nread)
partial = append(partial, buf[:nread]...)
parts := bytes.Split(partial, []byte{'\n'})
for i := 0; i < len(parts)-1; i++ {
line := strings.TrimSpace(string(parts[i]))
if line == "" {
continue
}
ring.Push(line)
select {
case broadcastCh <- line:
default:
}
}
incmp := strings.TrimSpace(string(parts[len(parts)-1]))
if len(incmp) > 0 {
ring.Push(incmp)
select {
case broadcastCh <- incmp:
default:
}
partial = []byte{}
}
}
// set up fsnotify
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Println("fsnotify.NewWatcher error:", err)
return
}
if err := watcher.Add(filepath); err != nil {
_ = watcher.Add(".") // if filepath not found, scan the directory for it
}
go func() {
readNew() // poll once at start to capture writes during startup
for {
select {
case ev := <-watcher.Events:
if ev.Op&fsnotify.Write == fsnotify.Write {
// small debounce so writes coalesced
time.Sleep(20 * time.Millisecond)
readNew()
}
case err := <-watcher.Errors:
log.Println("watcher error:", err)
}
}
}()
}
func serveLogPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "log.html")
}
func main() {
filepath := "temp.log"
if len(os.Args) > 1 {
filepath = os.Args[1]
}
// bootstrap last N lines
ring := NewRing(linesToKeep)
if last, err := ReadLastN(filepath, linesToKeep, chunkSize); err == nil && len(last) > 0 {
for _, l := range last {
ring.Push(l)
}
}
go broadcaster(ring)
// watcher that pushes incoming lines to broadcastCh and ring
startWatcher(filepath, ring)
http.HandleFunc("/log", serveLogPage)
http.HandleFunc("/ws", serveWS())
addr := ":8080"
fmt.Println("Listening on", addr, "serving /log and /ws — watching", filepath)
if err := http.ListenAndServe(addr, nil); err != nil {
log.Fatal(err)
}
}
<!-- html webpage -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Log Viewer</title>
</head>
<body style="font-family: monospace">
<h2>Live Log Viewer</h2>
<pre
id="out"
style="height: 70vh; overflow: auto; border: 1px solid #ddd; padding: 8px"
></pre>
<script>
let out = document.getElementById("out");
let ws = new WebSocket(
(location.protocol === "https:" ? "wss://" : "ws://") +
location.host +
"/ws"
);
ws.onmessage = function (evt) {
let t = document.createTextNode(evt.data + "\n");
out.appendChild(t);
out.scrollTop = out.scrollHeight;
};
ws.onopen = function () {
console.log("ws open");
};
ws.onclose = function () {
console.log("ws close");
};
ws.onerror = function (e) {
console.error("ws err", e);
};
</script>
</body>
</html>
I got the job :)
Top comments (0)