Skip to content

Commit

Permalink
replace logrus with log/slog from stdlib (#55)
Browse files Browse the repository at this point in the history
* added loggers to the various structs. code runs.

* ignore .idea (Goland)

* just needs 1.20 (slog)

* wire up the logging

* re-order code for readability

* pass the logger down the chains as things initialize.

* use slog directly.

* config now has multiple members.

* add logger to config

* add logger to event_stream.

* remove the attachlogger, it was fugly

* set up some logging in the hello world example.

* add a section about logging to the README. Reformat long lines for easier editiing.

* bump go version.

---------

Co-authored-by: Anthony De Meulemeester <[email protected]>
  • Loading branch information
perbu and anthdm authored Nov 14, 2023
1 parent d40a37c commit 58f1195
Show file tree
Hide file tree
Showing 21 changed files with 266 additions and 189 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ bin
TODO.txt
TODO
_test
.vscode
/.vscode
/.idea
101 changes: 74 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,25 @@

# Blazingly fast, low latency actors for Golang

Hollywood is an ULTRA fast actor engine build for speed and low-latency applications. Think about game servers, advertising brokers, trading engines, etc... It can handle **10 million messages in under 1 second**.
Hollywood is an ULTRA fast actor engine build for speed and low-latency applications. Think about game servers,
advertising brokers, trading engines, etc... It can handle **10 million messages in under 1 second**.

## What is the actor model?

The Actor Model is a computational model used to build highly concurrent and distributed systems. It was introduced by Carl Hewitt in 1973 as a way to handle complex systems in a more scalable and fault-tolerant manner.
The Actor Model is a computational model used to build highly concurrent and distributed systems. It was introduced by
Carl Hewitt in 1973 as a way to handle complex systems in a more scalable and fault-tolerant manner.

In the Actor Model, the basic building block is an actor, called receiver in Hollywood, which is an independent unit of computation that communicates with other actors by exchanging messages. Each actor has its own state and behavior, and can only communicate with other actors by sending messages. This message-passing paradigm allows for a highly decentralized and fault-tolerant system, as actors can continue to operate independently even if other actors fail or become unavailable.
In the Actor Model, the basic building block is an actor, called receiver in Hollywood, which is an independent unit of
computation that communicates with other actors by exchanging messages. Each actor has its own state and behavior, and
can only communicate with other actors by sending messages. This message-passing paradigm allows for a highly
decentralized and fault-tolerant system, as actors can continue to operate independently even if other actors fail or
become unavailable.

Actors can be organized into hierarchies, with higher-level actors supervising and coordinating lower-level actors. This allows for the creation of complex systems that can handle failures and errors in a graceful and predictable way.
Actors can be organized into hierarchies, with higher-level actors supervising and coordinating lower-level actors. This
allows for the creation of complex systems that can handle failures and errors in a graceful and predictable way.

By using the Actor Model in your application, you can build highly scalable and fault-tolerant systems that can handle a large number of concurrent users and complex interactions.
By using the Actor Model in your application, you can build highly scalable and fault-tolerant systems that can handle a
large number of concurrent users and complex interactions.

## Features

Expand Down Expand Up @@ -44,9 +52,10 @@ go get github.com/anthdm/hollywood/...

# Quickstart

> The **[examples](https://github.com/anthdm/hollywood/tree/master/examples)** folder is the best place to learn and explore Hollywood.
> The **[examples](https://github.com/anthdm/hollywood/tree/master/examples)** folder is the best place to learn and
> explore Hollywood.
```Go
```go
type message struct {
data string
}
Expand Down Expand Up @@ -78,8 +87,8 @@ func main() {

## Spawning receivers with configuration

```Go
e.Spawn(newFoo, "foo",
```go
e.Spawn(newFoo, "foo",
actor.WithMaxRestarts(4),
actor.WithInboxSize(1024 * 2),
actor.WithTags("bar", "1"),
Expand Down Expand Up @@ -125,44 +134,82 @@ time.Sleep(time.Second)

## Customizing the Engine

```Go
cfg := actor.Config{
```go
cfg := actor.Config{
PIDSeparator: "->",
}
e := actor.NewEngine(cfg)
}
e := actor.NewEngine(cfg)
```

After configuring the Engine with a custom PID Separator the string representation of PIDS will look like this:

```Go
pid := actor.NewPID("127.0.0.1:3000", "foo", "bar", "baz", "1")
// 127.0.0.1:3000->foo->bar->baz->1
```go
pid := actor.NewPID("127.0.0.1:3000", "foo", "bar", "baz", "1")
// 127.0.0.1:3000->foo->bar->baz->1
```

Note that you can also provide a custom logger to the engine. See the Logging section for more details.

## Custom middleware

You can add custom middleware to your Receivers. This can be usefull for storing metrics, saving and loading data for your Receivers on `actor.Started` and `actor.Stopped`.
You can add custom middleware to your Receivers. This can be usefull for storing metrics, saving and loading data for
your Receivers on `actor.Started` and `actor.Stopped`.

For examples on how to implement custom middleware, check out the middleware folder in the **[examples](https://github.com/anthdm/hollywood/tree/master/examples/middleware)**
For examples on how to implement custom middleware, check out the middleware folder in the *
*[examples](https://github.com/anthdm/hollywood/tree/master/examples/middleware)**

## Logging

You can set the log level of Hollywoods log module:
The default for Hollywood is, as any good library, not to log anything, but rather to rely on the application to
configure logging as it sees fit. However, as a convenience, Hollywood provides a simple logging package that
you can use to gain some insight into what is going on inside the library.

```Go
import "github.com/anthdm/hollywood/log
When you create a Hollywood engine, you can provide an optional actor configuration. This gives you the opportunity to
have the log package create a suitable logger. The logger will be based on the standard library's `log/slog` package.

log.SetLevel(log.LevelInfo)
```
If you want Hollywood to log with its defaults, it will provide structured logging with the loglevel being `ÌNFO`.
You'll then initialize the engine as such:

To disable all logging
```go
engine := actor.NewEngine(actor.Config{Logger: log.Default()})
```

```Go
import "github.com/anthdm/hollywood/log
If you want more control, say by having having the loglevel be DEBUG and the output format be JSON, you can do so by

log.SetLevel(log.LevelPanic)
```go
lh := log.NewHandler(os.Stdout, log.JsonFormat, slog.LevelDebug)
engine := actor.NewEngine(actor.Config{Logger: log.NewLogger("[engine]", lh)})
```

This will have the engine itself log with the field "log", prepopulated with the value "[engine]" for the engine itself.
The various subsystems will change the log field to reflect their own name.

### Log levels

The log levels are, in order of severity:

* `slog.LevelDebug`
* `slog.LevelInfo`
* `slog.LevelWarn`
* `slog.LevelError`

### Log components.

The log field "log" will be populated with the name of the subsystem that is logging. The subsystems are:

* `[engine]`
* `[context`
* `[deadLetter]`
* `[eventStream]`
* `[registry]`
* `[stream_reader]`
* `[stream_writer]`
* `[stream_router]`

In addition, the logger will log with log=$ACTOR_NAME for any actor that has a name.

See the log package for more details about the implementation.

# Test

```
Expand Down
7 changes: 4 additions & 3 deletions _bench/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package main

import (
"fmt"
"log/slog"
"os"
"runtime"
"time"

"github.com/anthdm/hollywood/actor"
"github.com/anthdm/hollywood/log"
"github.com/anthdm/hollywood/remote"
)

Expand Down Expand Up @@ -55,9 +56,9 @@ func benchmarkLocal() {

func main() {
if runtime.GOMAXPROCS(runtime.NumCPU()) == 1 {
log.Fatalw("Please use a system with more than 1 CPU. Its 2023...", nil)
slog.Error("GOMAXPROCS must be greater than 1")
os.Exit(1)
}
log.SetLevel(log.LevelPanic)
benchmarkLocal()
benchmarkRemote()
}
6 changes: 3 additions & 3 deletions actor/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ type Context struct {
// when the child dies.
parentCtx *Context
children *safemap.SafeMap[string, *PID]
logger log.Logger
}

func newContext(e *Engine, pid *PID) *Context {
return &Context{
engine: e,
pid: pid,
children: safemap.New[string, *PID](),
logger: e.logger.SubLogger("[context]"),
}
}

Expand All @@ -43,9 +45,7 @@ func (c *Context) Request(pid *PID, msg any, timeout time.Duration) *Response {
// Respond will sent the given message to the sender of the current received message.
func (c *Context) Respond(msg any) {
if c.sender == nil {
log.Warnw("[RESPOND] context got no sender", log.M{
"pid": c.PID(),
})
c.logger.Warnw("context got no sender", "func", "Respond", "pid", c.PID())
return
}
c.engine.Send(c.sender, msg)
Expand Down
12 changes: 7 additions & 5 deletions actor/deadletter.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,23 @@ import (
type deadLetter struct {
eventStream *EventStream
pid *PID
logger log.Logger
}

func newDeadLetter(eventStream *EventStream) *deadLetter {
return &deadLetter{
eventStream: eventStream,
pid: NewPID(LocalLookupAddr, "deadLetter"),
logger: eventStream.logger.SubLogger("[deadLetter]"),
}
}

func (d *deadLetter) Send(dest *PID, msg any, sender *PID) {
log.Warnw("[DEADLETTER]", log.M{
"dest": dest,
"msg": reflect.TypeOf(msg),
"sender": sender,
})
d.logger.Warnw("Send",
"dest", dest,
"msg", reflect.TypeOf(msg),
"sender", sender,
)
d.eventStream.Publish(&DeadLetterEvent{
Target: dest,
Message: msg,
Expand Down
17 changes: 9 additions & 8 deletions actor/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Config struct {
// pid := NewPID("127.0.0.1:4000", "foo", "bar")
// 127.0.0.1:4000/foo/bar
PIDSeparator string
Logger log.Logger
}

// Engine represents the actor engine.
Expand All @@ -38,17 +39,17 @@ type Engine struct {
address string
remote Remoter
deadLetter Processer
logger log.Logger
}

// NewEngine returns a new actor Engine.
func NewEngine(cfg ...Config) *Engine {
e := &Engine{
EventStream: NewEventStream(),
address: LocalLookupAddr,
}
e := &Engine{}
if len(cfg) == 1 {
e.configure(cfg[0])
}
e.EventStream = NewEventStream(e.logger)
e.address = LocalLookupAddr
e.Registry = newRegistry(e)
e.deadLetter = newDeadLetter(e.EventStream)
e.Registry.add(e.deadLetter)
Expand All @@ -59,6 +60,7 @@ func (e *Engine) configure(cfg Config) {
if cfg.PIDSeparator != "" {
pidSeparator = cfg.PIDSeparator
}
e.logger = cfg.Logger
}

// WithRemote returns a new actor Engine with the given Remoter,
Expand All @@ -85,7 +87,7 @@ func (e *Engine) SpawnFunc(f func(*Context), id string, opts ...OptFunc) *PID {
return e.Spawn(newFuncReceiver(f), id, opts...)
}

// SpawnProc spawns the give Processer. This function is usefull when working
// SpawnProc spawns the give Processer. This function is useful when working
// with custom created Processes. Take a look at the streamWriter as an example.
func (e *Engine) SpawnProc(p Processer) *PID {
e.Registry.add(p)
Expand Down Expand Up @@ -132,9 +134,8 @@ func (e *Engine) send(pid *PID, msg any, sender *PID) {
return
}
if e.remote == nil {
log.Errorw("[ENGINE] failed sending messsage", log.M{
"err": "engine has no remote configured",
})
e.logger.Errorw("failed sending messsage",
"err", "engine has no remote configured")
return
}
e.remote.Send(pid, msg, sender)
Expand Down
26 changes: 14 additions & 12 deletions actor/event_stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ type EventSub struct {
type EventStreamFunc func(event any)

type EventStream struct {
mu sync.RWMutex
subs map[*EventSub]EventStreamFunc
mu sync.RWMutex
subs map[*EventSub]EventStreamFunc
logger log.Logger
}

func NewEventStream() *EventStream {
func NewEventStream(l log.Logger) *EventStream {
return &EventStream{
subs: make(map[*EventSub]EventStreamFunc),
subs: make(map[*EventSub]EventStreamFunc),
logger: l.SubLogger("[eventStream]"),
}
}

Expand All @@ -31,10 +33,10 @@ func (e *EventStream) Unsubscribe(sub *EventSub) {

delete(e.subs, sub)

log.Tracew("[EVENTSTREAM] unsubscribe", log.M{
"subs": len(e.subs),
"id": sub.id,
})
e.logger.Debugw("unsubscribe",
"subs", len(e.subs),
"id", sub.id,
)
}

func (e *EventStream) Subscribe(f EventStreamFunc) *EventSub {
Expand All @@ -46,10 +48,10 @@ func (e *EventStream) Subscribe(f EventStreamFunc) *EventSub {
}
e.subs[sub] = f

log.Tracew("[EVENTSTREAM] subscribe", log.M{
"subs": len(e.subs),
"id": sub.id,
})
e.logger.Debugw("subscribe",
"subs", len(e.subs),
"id", sub.id,
)

return sub
}
Expand Down
Loading

0 comments on commit 58f1195

Please sign in to comment.