{"id":21452929,"url":"https://github.com/arindas/proglog","last_synced_at":"2026-03-07T11:32:21.754Z","repository":{"id":43379959,"uuid":"466044837","full_name":"arindas/proglog","owner":"arindas","description":"Distributed Commit Log from Travis Jeffery's Distributed Services book","archived":false,"fork":false,"pushed_at":"2022-05-23T16:07:51.000Z","size":238,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-22T10:45:58.140Z","etag":null,"topics":["distributed-log","distributed-systems","golang","gossip-protocol","grpc","raft"],"latest_commit_sha":null,"homepage":"https://pkg.go.dev/github.com/arindas/proglog","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/arindas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-03-04T08:36:07.000Z","updated_at":"2024-12-21T12:17:21.000Z","dependencies_parsed_at":"2022-09-11T04:21:07.196Z","dependency_job_id":null,"html_url":"https://github.com/arindas/proglog","commit_stats":null,"previous_names":[],"tags_count":30,"template":false,"template_full_name":null,"purl":"pkg:github/arindas/proglog","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arindas%2Fproglog","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arindas%2Fproglog/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arindas%2Fproglog/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arindas%2Fproglog/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/arindas","download_url":"https://codeload.github.com/arindas/proglog/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arindas%2Fproglog/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30212124,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-07T09:02:10.694Z","status":"ssl_error","status_checked_at":"2026-03-07T09:02:08.429Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["distributed-log","distributed-systems","golang","gossip-protocol","grpc","raft"],"created_at":"2024-11-23T04:33:09.201Z","updated_at":"2026-03-07T11:32:21.719Z","avatar_url":"https://github.com/arindas.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# proglog\n[![Go Report Card](https://goreportcard.com/badge/github.com/arindas/proglog)](https://goreportcard.com/report/github.com/arindas/proglog)\n[![ci-tests](https://github.com/arindas/proglog/actions/workflows/ci-tests.yml/badge.svg)](https://github.com/arindas/proglog/actions/workflows/ci-tests.yml)\n[![codecov](https://codecov.io/gh/arindas/proglog/branch/main/graph/badge.svg?token=9YI8TVJ2U4)](https://codecov.io/gh/arindas/proglog)\n[![Go Reference](https://pkg.go.dev/badge/github.com/arindas/proglog.svg)](https://pkg.go.dev/github.com/arindas/proglog)\n![LOC](https://sloc.xyz/github/arindas/proglog)\n![GitHub repo size](https://img.shields.io/github/repo-size/arindas/proglog)\n[![mit-license](https://img.shields.io/badge/License-MIT-green.svg)](https://img.shields.io/badge/License-MIT-green.svg)\n---\n\n\u003c/div\u003e\n\nA distributed commit log.\n\nThis repository follows the book \n[\"Distributed Services with Go\"](https://pragprog.com/titles/tjgo/distributed-services-with-go/) \nby Travis Jeffrey.\n\nThe official repository for this book can be found at https://github.com/travisjeffery/proglog \n\nThis repository is meant for a personal record of working through the book. Progress through\ndifferent sections of the book are marked with tags. We create a new tag on the completion of a \nspecific section. If you want to work on a specific section,\ncheckout to the previous tag, create a new branch from it and start working.\n\nWhere necessary, I have used my preferred idioms and style when implementing a specific section,\nwhile still adhering to correctness with all the tests. Please keep this in mind if you plan to\nuse this as a reference. If you prefer the original source, refer to the official repository instead.\n\n## Installation\n\n### Using pre-built binaries\nSimply download the binary archive from the latest release and copy them to a location on your path.\nFor instance, installing proglog v0.10.11 on linux amd64 would be as simple as this:\n```\nwget https://github.com/arindas/proglog/releases/download/v0.10.11/proglog-v0.10.11-linux-amd64.tar.gz\ntar xzf proglog-v0.10.11-linux-amd64.tar.gz \ncp ./proglog ~/.local/bin\n```\n\n### Build from source\nSimply install the `proglog` command binary as follows:\n```\ngo install github.com/arindas/proglog/cmd/proglog@latest\n```\n\n### Docker\nYou may also pull the docker image as follows:\n```\ndocker pull ghcr.io/arindas/proglog:latest\n```\n\n## Usage\n```\n$ ./proglog --help\nUsage:\n  proglog [flags]\n\nFlags:\n      --acl-model-file string         Path to ACL model.\n      --acl-policy-file string        Path to ACL policy.\n      --bind-addr string              Address to bind Serf on (default \"127.0.0.1:8401\")\n      --bootstrap                     Bootstrap the cluster.\n      --config-file string            Path to config file.\n      --data-dir string               Directory to store log and Raft data (default \"/tmp/proglog\")\n  -h, --help                          help for proglog\n      --node-name string              Unique server ID. (default \"arubox\")\n      --peer-tls-ca-file string       Path to peer certificate authority.\n      --peer-tls-cert-file string     Path to peer tls cert.\n      --peer-tls-key-file string      Path to peer tls key.\n      --rpc-port int                  Port for RPC clients (and Raft) connections (default 8400)\n      --server-tls-ca-file string     Path to server certificate authority.\n      --server-tls-cert-file string   Path to server tls cert.\n      --server-tls-key-file string    Path to server tls key.\n      --start-join-addrs strings      Serf addresses to join.\n```\nOne can either pass in the path of the config file, or pass in all the command line flags.\n\n## Testing\nRunning the tests requires the ca certificates to be generated for ssl, and copied to a central\nlocation, along with ACL policy and model files. We follow XDG conventions and copy the files\nto `${HOME}/.config/proglog`. We also require `cfssl` and `cfssljson` for generating the certificates.\n\nSetup your environment and run the tests as follows:\n```\ngit clone https://github.com/arindas/proglog.git; cd proglog/\n\n# assuming go is already installed and ${GOBIN} is added to ${PATH}\ngo get github.com/cloudflare/cfssl/cmd/...\n\nmake gencert\nmake genacl\nmake test\n```\n\n## License\nThis repository is presented under the MIT License. See [LICENSE](./LICENSE) for more details.\n\n## Changelog\n\n### v0.10.0~11 Chapter 10 - Deploy to k8s; `proglog` command\nThe command simply creates a new `Agent` instance with `agent.New(agent.Config)` and loads\nconfiguration values from command line flags or config files; whichever is available.\n\nWe also created a Dockerfile for containerizing our service along with a workflow to\npush images to github's docker image registry.\n\n### v0.9.2~3 Migrated to a single module repo; Doc updates\n\n### v0.9.1 Chapter 9 - Client side load balancing: Picker\nImplemented the load balancing component, which picks which sub connection to use from all the \nconnections available for a pariticular requests. (Each server in the cluster is mapped to a \nsub connection). We implemented a `Picker` entity with the following interface:\n```go \n// Picker represents an entity for picking which connection to use for a request.\n// It is the load balancing component of the gRPC request resolution process.\ntype Picker struct { … }\n\n// Seperates the connections to the followers from the connection to the leader\n// and stores them in a Picker instance. The picker instance built is returned.\nfunc (p *Picker) Build(buildInfo base.PickerBuildInfo) balancer.Picker { … }\n\n// Picks the subconnection to use for the given request. All writes(i.e. Produce)\n// go through the leader. Read(i.e Consume) requests are balanced among followers\n// in a round robin fashion.\n//\n// Returns the subconnection picked, along with an error if any.\nfunc (p *Picker) Pick(info balancer.PickInfo) (balancer.PickResult, error) { … }\n```\n\n### v0.9.0 Chapter 9 - Client side load balancing: Resolver\nClient side load balancing empowers clients to decide how to balance reads and writes across\nmultiple instances of our services. In our case, all writes first go through the leader, and\nare replicated to the rest of the nodes. Reads, however, can me made from any node, since all nodes\nare in consensus on the data. Hence we need to incorporate this behavior to our client side load\nbalancing.\n\nWe could opt for load balancing through a reverse proxy or a separate service altogether. Now,\nsince Raft keeps track of all the servers in the cluster, we can obtain the details of the servers\nin the cluster from any node's raft instance. That way, any client could obtain the information\nof all the servers in the cluster, which would allow them to perform client side load balancing.\nThe advantage here, is that we don't need a separate service for load balancing.\n\nFirst we provide a new API for obtaining all the servers from a single server in a cluster:\n```go\n// Returns a slice of all the servers in the cluster of which this\n// server is a member.\nfunc (l *DistributedLog) GetServers() ([]*api.Server, error) { … }\n```\n\nHowever even before that we created a new data model for representing servers:\n```proto \nmessage Server {\n    string id = 1;\n    string rpc_addr = 2;\n    bool is_leader = 3;\n}\n```\n\nNow once we had a mechanism for obtaining the servers, we created a new gRPC endpoint:\n```proto \nmessage GetServersRequest {}\nmessage GetServersResponse { repeated Server servers = 1; }\n```\n\nFinally we implemented a custom service resolver for our gRPC client to provide the gRPC\nclient with all available servers in our Raft cluster. The actual load balancing behavior\nis implemented with a `Picker`.\n\nHere's the API provided by our `Resolver`:\n```go \ntype Resolver struct { … }\n\n// Name to use for our scheme for gRPC to filter out and resolve to our resolver.\n// Our target server addresses will be formatted like \"proglog://our-service-address\"\nfunc (r *Resolver) Scheme() string { … }\n\n// Sets up a client connection for querying details of servers in the cluster.\nfunc (r *Resolver) Build(\n\ttarget resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions,\n) (resolver.Resolver, error) { … }\n\n// Fetches the list of servers with a GetServersRequest api call, and obtains\n// the resolved addresses to use.\nfunc (r *Resolver) ResolveNow(resolver.ResolveNowOptions) { … }\n\n// Closes the resolver connection.\nfunc (r *Resolver) Close() { … }\n \n```\n\n\n### v0.8.3 Chapter 8 - Replace dumb replication with Raft replication in Agent\nIncorporated the Raft based replication system in the agent.\n\n### v0.8.1~2 Updated deps for internal/{server,discovery}\n\n### v0.8.0 Chapter 8 - Raft Consensus\nImplemented a distributed version of our commit log, where the replication is governed by the Raft\nconsensus protocol. There is a strict leader-follower relationship between the servers in the cluster\nand records are only copied from the leader to the follower.\n\nA single `DistributedLog` entity was introduced. This entity encapsulated together a commit log instance,\nand a Raft instance. We present the following API:\n\n```go\n// Allocates the local log's data structures and creates this log's\n// backing files in the given directory.\n//\n// Configures replication with the raft consensus protocol.\n// A raft instance contains four components:\n// - A finite state machine which represents the state of\n//  the raft instance. All raft commands are applied to the\n//  finite state machine. The FSM then inteprets the command\n//  accordingly to produce desired side effects and goto the\n//  desired state.\n// - A log store for storing the commands to be applied\n//  (distinct from our actual record log store)\n// - A stable key value store for storing Raft's configuration\n//  and cluster data. (e.g addresses of other servers in the\n//  cluster)\n// - A file snapshot store for storing data snapshots. These\n//  are used for data recovery in the event of server failures.\n// - A network streaming layer for connecting to other servers\n//  in the cluster.\n//\n// The provided directory path is used for creating all the data\n// stores. All persistent data is stored in this directory.\nfunc NewDistributedLog(dataDir string, config Config) (*DistributedLog, error) { … }\n\n// Applies an \"AppendRecord(record)\" command to the local raft instance. The raft instance\n// records this command, and first replicates this to all followers. Once this command\n// has been replicated to the majority of the followers, the command is committed by\n// applying it to the local Raft FSM. The FSM produces the desired side effect (appending\n// the record to the local log). Once the command is commited, the leader again requests\n// all the followers to commit the command.\n// This way the record is appended to the leader and all the followers consistently.\nfunc (l *DistributedLog) Append(record *api.Record) (uint64, error) { … }\n\n// Reads the record from the local log instance at given offset.\nfunc (l *DistributedLog) Read(offset uint64) (*api.Record, error) { … }\n\n// Invoked on the leader to join a Raft cluster. Adds the server with given id and address\n// as a voter to cluster of which the invocated server is a leader.\nfunc (l *DistributedLog) Join(id, addr string) error { … }\n\n// Invoked on the leader to remove the server with the given id from the cluster.\nfunc (l *DistributedLog) Leave(id string) error { … }\n\n// Waits for leader to be elected synchronously.\n// We check every second upto the given timeout duration whether a leader\n// has been elected or not. If the leader is elected at some tick second\n// we return. Otherwise we return after the timeout duration with an error.\n//\n// This method is mostly useful in tests.\nfunc (l *DistributedLog) WaitForLeader(timeout time.Duration) error { … }\n\n// Shutsdown the associated raft instances and closes the underlying commit log.\nfunc (l *DistributedLog) Close() error { … }\n```\n\n### v0.7.1 Chapter 7 - Replication, Log Service Agent\nImplemented log replication: consume logs from every peer in the cluster and produce them locally. (This\nbehavior leads to infinite replication of the same record since there is no well defined leader-follower\nrelationship. The original producer, ends up consuming the same record from another peer which consumed\nthe record from itself.)\n\nOrchestrated the different components of our log service using a single 'Agent' entity:\n\n```go\ntype Agent struct {\n  Config\n\n  // … unexported members\n}\n```\n`Agent` requires the following configuration:\n```go\n// Represents the configuration for our Agent.\ntype Config struct {\n\tServerTLSConfig *tls.Config // TLS authentication config for server\n\tPeerTLSConfig   *tls.Config // TLS authentication config for peers\n\n\tDataDir string // Data directory for storing log records\n\n\tBindAddr       string   // Address of socket used for listening to cluster membership events\n\tRPCPort        int      // Port used for serving log service GRPC requests\n\tNodeName       string   // Node name to use for cluster membership\n\tStartJoinAddrs []string // Addresses of nodes from the cluster. Used for joining the cluster\n\n\tACLModelFile  string // Access control list model file for authorization\n\tACLPolicyFile string // Access control list policy file for authorization\n}\n```\n\nIt exposes the following methods:\n```go\n// RPC Socket Address with format \"{BindAddrHost}:{RPCPort}\"\n// BindAddr and RPCAddr share the same host.\nfunc (c Config) RPCAddr() (string, error) { … }\n\n// Shuts down the commit log service agent. The following steps are taken: Leave Cluster, Stop record\n// replication, gracefully stop RPC server, cleanup data structures for the commit log. This method\n// retains the files written by the log service since they might be necessary for data recovery.\n// Returns any error which occurs during the shutdown process, nil otherwise.\nfunc (a *Agent) Shutdown() error { … }\n\n// Constructs a new Agent instance. It take the following steps for setting up an Agent:\n// Setup application logging, created data-structures for the commit log, setup the RPC\n// server and finally start the cluster membership manager.\n// Returns any error which occurs during the membership setup, nil otherwise.\n//\n// Sets up cluster membership handlers for this commit log service. This method instantiates\n// the cluster membership handlers with that of the log replicator. This effectively allows this\n// commit log service instance to replicate records from all nodes and any new nodes that\n// joins the cluster, of which this service instance is a member. We also responsibly stop\n// replicating records from any node that leaves the cluster.\nfunc New(config Config) (*Agent, error) { … }\n\n```\n\n### v0.7.0 Chapter 7 - Service Discovery: Discover services with Serf\nImplemented a Serf cluster membership manager. It handles cluster membership events and manages cluster\nmembership operations. Membership event handling is made configurable with the following interface:\n\n```go\n// Handler for cluster membership modification operations for a Node.\ntype Handler interface {\n\tJoin(name, addr string) error\n\tLeave(name string) error\n}\n```\n\nOur Membership manager used the following configuration:\n```go\n// Configuration of a single node in a Surf cluster.\ntype Config struct {\n\t// NodeName acts as the node's unique identifier across the Serf cluster.\n\tNodeName string\n\n\t// Serf listens on this address for gossiping. [Ref: gossip protocol serf]\n\tBindAddr string\n\n\t// Used for sharing data to other nodes in the cluster. This information is\n\t// used by the cluster to decide how to handle this node.\n\tTags map[string]string\n\n\t// Used for joining a new node to the cluster. Joining a new node requires\n\t// pointing to atleast one in-cluster node. In a prod env., it's advisable\n\t// to specify at least 3 addrs to increase cluster resliency.\n\tStartJoinAddrs []string\n}\n```\n\nWhere \"name\" refers to node name and \"addr\" refers to the node RPC address.\n\nOn every node that it's configured in, our membership manager does the following operations:\n- Binds and listen's on a unique port on localhost for mebership events from the network. (As used by\nSerf's gossip protocol.)\n- Sets up serf config with the binded address and port, the node name and the tags\n- Routes mebership events from the network to a channel for easy consumption\n- Start listening for membership events from the routed channel\n- If addresses for Nodes in an existing cluster are provided via \"StartJoinAddrs\", we issue a join request\nwith the provided address to join the cluster. If not a new cluster is created with only the invoking node.\n\nOur membership managers are created with a simple constructor function:\n```go\n// Creates a new Serf cluster member with the given config and cluster handler.\n// It internally setups up the Serf configuration and event handlers.\nfunc New(handler Handler, config Config) (*Membership, error) { … }\n\n```\n\n\n### v0.6.0 Chapter 6 - Observe your systems\nImplemented tracing and metric collection for our service with OpenCensus. Added logging with Uber Zap.\nThis simply required us to setup and wire the required middlewares for each.\n\n### v0.5.1 Chapter 5 - Corrected github action to properly install cfssl* tools.\nHere's the github workflow step for generating the configs for testing authentication and authorization.\n\n```\n- name: Generate Config\n  run: |\n    go get github.com/cloudflare/cfssl/cmd/... \n    export PATH=${PATH}:${HOME}/go/bin\n    make gencert\n    make genacl\n```\n\n### v0.5.0 Chapter 5 - Secure our service\nImplemented TLS authentication for clients and servers. Added authorization support with Access control lists.\n\n```\nf879aaa Tested and verified that unauthorized clients are denied access\na39869e Moved configuration necessary for tests into testconf\n87fbe5f Moved to multiple clients to test ACL implementation.\n44d430e Implemented mutual TLS authentication for our GRPC Log Service in tests\n```\n\n### v0.4.0 Chapter 4 - Serve Requests with gRPC\nPresented the log as a gRPC service, with the following protobuf definitions:\n```proto\n// Messages\nmessage Record { bytes value = 1; uint64 offset = 2; }\n\nmessage ProduceRequest { Record record = 1; }\nmessage ProduceResponse { uint64 offset = 1; }\n\nmessage ConsumeRequest { uint64 offset = 1; }\nmessage ConsumeResponse { Record record = 2; }\n\n\n// Log Service\nservice Log {\n    rpc Produce(ProduceRequest) returns (ProduceResponse) {}\n    rpc Consume(ConsumeRequest) returns (ConsumeResponse) {}\n    rpc ConsumeStream(ConsumeRequest) returns (stream ConsumeResponse) {}\n    rpc ProduceStream(stream ProduceRequest) returns (stream ProduceResponse) {}\n}\n```\n\n`Produce` and `Consume` are simply wrappers to a log implementation which produce a record and consume\na record from the underlying log.\n\n`ProduceStream` uses a bi-directional stream. It reads records from the stream, produces them to the\nlog, and writes the returned offsets to the stream.\n\n`ConsumeStream` simply accepts an offset and returns a read only stream to read records starting from\nthe given offset to the end of the log.\n   \n\n### v0.3.3 Chapter 3 - Completed the Log Implementation.\nA log is paritioned into a collection of segments, sorted by the offsets\nof the records they contain. The last segment is the active segment.\n\nWrites goto the last segment till it's capacity is maxed out. Once it's\ncapacity is full, we create new segment and set it as the active segment.\n\nRead operations are serviced by a linear search on the segments to find\nthe segment which contains the given offset. If the segment is found, we\nsimply utilize its Read() operation to read the record.\n\n### v0.3.2 Chapter 3 - Add a segment impl for commit log package\nSegment represents a Log segment with a store file and a index to speed up reads. It is\nthe data structure to unify data store and indexes.\n\nThe segment implementation provides the following API:\n- `Append(record *api.Record) (offset uint64, err error)` \n  Appends a new record to this segment. Returns the absolute offset of\n  the newly written record along with an error if any.\n\n- `Read(off uint64) (*api.Record, error)`\n  Reads the record at the given absolute offset. Returns the record read\n  along with an error if any.\n\n### v0.3.1 Chapter 3 - Add a index impl for commit log package\nImplemented an index containing \u003csegment relative offset, position in store file\u003e entries.\nIt is backed by a file on disk, which is mmap()-ed for frequent access.\n\nReal offsets are translated using segment relative offset + segment start offset. The\nsegment start offset is stored in the config.\n\nThe index implementation provides the following API:\n- `Read(in int64) (off uint32, pos uint64, err error)`\n  Reads and returns the segment relative offset and position in store for a record,\n  along with an error if any.\n  The parameter integer is interpreted as the sequence number. In case of negative\n  values, it is interpreted from the end.\n  (0 indicates first record, -1 indicates the last record)\n \n- `Write(off uint32, pos uint64) error`\n  Writes the given offset and the position pair at the end of the index file.\n  Returns an error if any.\n  \n  The offset and the position are binary encoded and written at the end of\n  the file. If the file doesn't have enough space to contain 12 bytes, EOF\n  is returned. We also increase the size of the file by 12 bytes, post writing.\n\n### v0.3.0 Chapter 3 - Add a store impl for commit log package\nCreated a buffered store implementation backed by a file for a commit log package.\n\nThe store implementation is intended to be package local and provides the following API:\n- `Append([]byte) bytesWritten uint64, position uint64, err error`\n  Appends the given bytes as a record to this store. Returns the number of bytes written,\n  the position at which is written along with errors if any.\n- `Read(pos uint64) []byte, error`\n  Reads the record at the record at the given position and returns the bytes in the record\n  along with an error if any.\n- `Close() error`\n  Closes the store by closing the underlying the file instance.\n\nAdded tests for verifying the correctness of the implementation.\n\n### v0.2.0 Chapter 2 - Add a protobuf definition for Record\nCreated a protobuf definition for log record in [log.proto](./api/v1/log.proto) and generated\ncorresponding go stubs for it using `protoc`\n\nCreated a convenience Makefile for easily generating go stubs in the future.\n\n### v0.1.0 Chapter 1 - Basic append only log implementation\nImplemented as basic append only log and presented it with a simple REST API\n\nThe Log provides two operations:\n- `Read(Offset) Record`\n  Reads a record from the given offset or returns a record not found error.\n- `Append(Record) Offset`\n  Appends the given record at the end of the log and returns the offset at\n  which it was written in the log.\n\nWe expose this log as REST API with the following methods:\n- `[POST /] { record: []bytes } =\u003e { offset: uint64 }`\n  Appends the record provided in json request body and returns the offset at\n  which it was written in json response.\n- `[GET /] { offset: uint64 } =\u003e { record: []bytes }`\n  Responds with record at the offset in the request.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farindas%2Fproglog","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farindas%2Fproglog","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farindas%2Fproglog/lists"}