Files
tiennm99 afaa631c94 refactor(scribe): normalize ASCII formatting
Remove decorative Unicode banner comments, replace em-dashes with
hyphens across Dockerfile, configs, docs, and one Go log string,
alphabetize the builder-stage yum package list, and drop the
non-standard description/version LABELs from the runtime stage.
2026-05-28 12:33:25 +07:00

234 lines
6.5 KiB
Go

// Command scribe-example-sender pushes a few LogEntry messages to a Scribe
// daemon to demonstrate that the example writes real log files.
//
// It speaks Scribe's Thrift RPC by hand-encoding the framed binary `Log()` call
// using only the Go standard library - no Apache Thrift dependency and no
// generated bindings. The Scribe Thrift contract:
//
// enum ResultCode { OK = 0, TRY_LATER = 1 }
// struct LogEntry { 1: string category; 2: string message; }
// service scribe { ResultCode Log(1: list<LogEntry> messages); }
package main
import (
"bufio"
"bytes"
"encoding/binary"
"fmt"
"io"
"log"
"net"
"os"
"strconv"
"time"
)
// Thrift binary protocol type IDs and message types.
const (
tSTOP = 0
tI32 = 8
tSTRING = 11
tSTRUCT = 12
tLIST = 15
msgCALL = 1
)
func env(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func envInt(key string, def int) int {
if v := os.Getenv(key); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return def
}
// --- TBinaryProtocol writers (all integers big-endian / network order) ---
func writeI16(b *bytes.Buffer, v int16) {
var tmp [2]byte
binary.BigEndian.PutUint16(tmp[:], uint16(v))
b.Write(tmp[:])
}
func writeI32(b *bytes.Buffer, v int32) {
var tmp [4]byte
binary.BigEndian.PutUint32(tmp[:], uint32(v))
b.Write(tmp[:])
}
func writeString(b *bytes.Buffer, s string) {
writeI32(b, int32(len(s)))
b.WriteString(s)
}
func writeFieldBegin(b *bytes.Buffer, typeID byte, id int16) {
b.WriteByte(typeID)
writeI16(b, id)
}
// buildLogFrame builds a framed Thrift `Log()` CALL carrying one LogEntry per
// message, all under the same category.
//
// Uses the non-strict TBinaryProtocol message header (string name, byte type,
// i32 seqid) to match the README Python client (strictWrite=False); the Scribe
// C++ server reads with strict_read=false and accepts it.
func buildLogFrame(category string, messages []string) []byte {
var p bytes.Buffer
// messageBegin (non-strict): name, type, seqid
writeString(&p, "Log")
p.WriteByte(msgCALL)
writeI32(&p, 0)
// Log_args struct: field 1 = list<LogEntry> messages
writeFieldBegin(&p, tLIST, 1)
p.WriteByte(tSTRUCT) // list element type
writeI32(&p, int32(len(messages)))
for _, m := range messages {
writeFieldBegin(&p, tSTRING, 1) // LogEntry.category
writeString(&p, category)
writeFieldBegin(&p, tSTRING, 2) // LogEntry.message
writeString(&p, m)
p.WriteByte(tSTOP) // end LogEntry
}
p.WriteByte(tSTOP) // end Log_args
// Frame the payload with a 4-byte big-endian length prefix (TFramedTransport).
var frame bytes.Buffer
writeI32(&frame, int32(p.Len()))
frame.Write(p.Bytes())
return frame.Bytes()
}
// readResultCode reads one framed Thrift reply and extracts the ResultCode.
// Handles both strict and non-strict message headers, since the server's write
// strictness is independent of ours.
func readResultCode(r io.Reader) (int32, error) {
var lenBuf [4]byte
if _, err := io.ReadFull(r, lenBuf[:]); err != nil {
return -1, fmt.Errorf("read frame length: %w", err)
}
frame := make([]byte, binary.BigEndian.Uint32(lenBuf[:]))
if _, err := io.ReadFull(r, frame); err != nil {
return -1, fmt.Errorf("read frame body: %w", err)
}
br := bytes.NewReader(frame)
// messageBegin: detect strict vs non-strict from the first i32.
var i32buf [4]byte
if _, err := io.ReadFull(br, i32buf[:]); err != nil {
return -1, fmt.Errorf("read message header: %w", err)
}
if first := int32(binary.BigEndian.Uint32(i32buf[:])); first < 0 {
// strict: first = version|type, then string name, then i32 seqid
var nameLen [4]byte
if _, err := io.ReadFull(br, nameLen[:]); err != nil {
return -1, err
}
if _, err := io.CopyN(io.Discard, br, int64(binary.BigEndian.Uint32(nameLen[:]))); err != nil {
return -1, err
}
if _, err := io.ReadFull(br, i32buf[:]); err != nil { // seqid
return -1, err
}
} else {
// non-strict: first = name length, then name, then i8 type, then i32 seqid
if _, err := io.CopyN(io.Discard, br, int64(first)); err != nil {
return -1, err
}
if _, err := br.ReadByte(); err != nil { // message type
return -1, err
}
if _, err := io.ReadFull(br, i32buf[:]); err != nil { // seqid
return -1, err
}
}
// Log_result struct: first field is the return value (id 0, type I32).
typeID, err := br.ReadByte()
if err != nil {
return -1, err
}
if typeID == tSTOP {
return 0, nil // no return field present; treat as OK
}
var idBuf [2]byte
if _, err := io.ReadFull(br, idBuf[:]); err != nil {
return -1, err
}
if typeID != tI32 {
return -1, fmt.Errorf("unexpected reply field type %d", typeID)
}
if _, err := io.ReadFull(br, i32buf[:]); err != nil {
return -1, err
}
return int32(binary.BigEndian.Uint32(i32buf[:])), nil
}
// dialWithRetry retries the initial connection so a cold `docker compose up`
// does not fail before the Scribe daemon is listening.
func dialWithRetry(addr string, attempts int, interval time.Duration) (net.Conn, error) {
var lastErr error
for i := 0; i < attempts; i++ {
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err == nil {
return conn, nil
}
lastErr = err
log.Printf("scribe not reachable at %s (attempt %d/%d): %v", addr, i+1, attempts, err)
time.Sleep(interval)
}
return nil, lastErr
}
func main() {
host := env("SCRIBE_HOST", "scribe")
port := env("SCRIBE_PORT", "1463")
category := env("SCRIBE_CATEGORY", "example")
count := envInt("SCRIBE_COUNT", 5)
addr := net.JoinHostPort(host, port)
conn, err := dialWithRetry(addr, 10, 1*time.Second)
if err != nil {
log.Fatalf("could not connect to scribe at %s: %v", addr, err)
}
defer conn.Close()
// Bound the whole exchange so a wedged server that accepts the connection
// but never replies cannot hang this one-shot sender indefinitely.
conn.SetDeadline(time.Now().Add(10 * time.Second))
messages := make([]string, 0, count)
for i := 1; i <= count; i++ {
messages = append(messages, fmt.Sprintf("Hello from Go sender - message %d/%d", i, count))
}
if _, err := conn.Write(buildLogFrame(category, messages)); err != nil {
log.Fatalf("send failed: %v", err)
}
rc, err := readResultCode(bufio.NewReader(conn))
if err != nil {
log.Fatalf("read reply failed: %v", err)
}
switch rc {
case 0:
log.Printf("sent %d messages to category %q, result=OK", count, category)
case 1:
log.Printf("sent %d messages to category %q, result=TRY_LATER (scribe busy)", count, category)
os.Exit(2)
default:
log.Printf("sent %d messages to category %q, unexpected result code=%d", count, category, rc)
os.Exit(3)
}
}