Repository: Tochemey/goakt
Pull request #196 (2023-12-30 23:30:24)

feat: add metric instrumentation
#170 

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/Tochemey/goakt/pull/196?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene) Report
Attention: `21 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`f862fff`)](https://app.codecov.io/gh/Tochemey/goakt/commit/f862fff9dd627dd85a6f48e5f00ecf2926e08af1?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene) 90.18% compared to head [(`69ad42e`)](https://app.codecov.io/gh/Tochemey/goakt/pull/196?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene) 89.88%.

| [Files](https://app.codecov.io/gh/Tochemey/goakt/pull/196?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene) | Patch % | Lines |
|---|---|---|
| [metric/actor\_metric.go](https://app.codecov.io/gh/Tochemey/goakt/pull/196?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene#diff-bWV0cmljL2FjdG9yX21ldHJpYy5nbw==) | 68.08% | [10 Missing and 5 partials :warning: ](https://app.codecov.io/gh/Tochemey/goakt/pull/196?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene) |
| [metric/system\_metric.go](https://app.codecov.io/gh/Tochemey/goakt/pull/196?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene#diff-bWV0cmljL3N5c3RlbV9tZXRyaWMuZ28=) | 72.72% | [4 Missing and 2 partials :warning: ](https://app.codecov.io/gh/Tochemey/goakt/pull/196?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #196      +/-   ##
==========================================
- Coverage   90.18%   89.88%   -0.31%     
==========================================
  Files          42       44       +2     
  Lines        4667     4736      +69     
==========================================
+ Hits         4209     4257      +48     
- Misses        368      382      +14     
- Partials       90       97       +7     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Tochemey/goakt/pull/196?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene).


Patch:
diff --git a/actors/actor_system.go b/actors/actor_system.go
index b2c9cc2..198882d 100644
--- a/actors/actor_system.go
+++ b/actors/actor_system.go
@@ -43,14 +43,15 @@ import (
 	internalpb "github.com/tochemey/goakt/internal/v1"
 	"github.com/tochemey/goakt/internal/v1/internalpbconnect"
 	"github.com/tochemey/goakt/log"
+	"github.com/tochemey/goakt/metric"
 	addresspb "github.com/tochemey/goakt/pb/address/v1"
 	eventspb "github.com/tochemey/goakt/pb/events/v1"
 	"github.com/tochemey/goakt/pkg/types"
 	"github.com/tochemey/goakt/telemetry"
-	"go.opentelemetry.io/otel"
 	"go.opentelemetry.io/otel/codes"
-	"go.opentelemetry.io/otel/metric"
+	otelmetric "go.opentelemetry.io/otel/metric"
 	"go.opentelemetry.io/otel/trace"
+	"go.opentelemetry.io/otel/trace/noop"
 	"go.uber.org/atomic"
 	"golang.org/x/net/http2"
 	"golang.org/x/net/http2/h2c"
@@ -189,6 +190,8 @@ type actorSystem struct {
 	// specifies whether tracing is enabled
 	traceEnabled atomic.Bool
 	tracer       trace.Tracer
+	// specifies whether metric is enabled
+	metricEnabled atomic.Bool
 }
 
 // enforce compilation error when all methods of the ActorSystem interface are not implemented
@@ -224,13 +227,14 @@ func NewActorSystem(name string, opts ...Option) (ActorSystem, error) {
 		eventsStream:        eventstream.New(),
 		partitionHasher:     hash.DefaultHasher(),
 		actorInitTimeout:    DefaultInitTimeout,
-		tracer:              trace.NewNoopTracerProvider().Tracer(name),
+		tracer:              noop.NewTracerProvider().Tracer(name),
 	}
 	// set the atomic settings
 	system.hasStarted.Store(false)
 	system.remotingEnabled.Store(false)
 	system.clusterEnabled.Store(false)
 	system.traceEnabled.Store(false)
+	system.metricEnabled.Store(false)
 
 	// apply the various options
 	for _, opt := range opts {
@@ -239,11 +243,21 @@ func NewActorSystem(name string, opts ...Option) (ActorSystem, error) {
 
 	// set the tracer when tracing is enabled
 	if system.traceEnabled.Load() {
-		system.tracer = otel.GetTracerProvider().Tracer(name)
+		system.tracer = system.telemetry.Tracer
 	}
 
 	// set the message scheduler
 	system.scheduler = newScheduler(system.logger, system.shutdownTimeout, withSchedulerCluster(system.cluster))
+
+	// set metric and return the registration error in case there is one
+	// re-register metric when metric is enabled
+	if system.metricEnabled.Load() {
+		// log the error but don't panic
+		if err := system.registerMetrics(); err != nil {
+			return nil, err
+		}
+	}
+
 	return system, nil
 }
 
@@ -404,6 +418,11 @@ func (x *actorSystem) Spawn(ctx context.Context, name string, actor Actor) (PID,
 		opts = append(opts, withTracing())
 	}
 
+	// set the pid metric option
+	if x.metricEnabled.Load() {
+		opts = append(opts, withMetric())
+	}
+
 	// create an instance of the actor ref
 	pid, err := newPID(spanCtx,
 		actorPath,
@@ -1146,7 +1165,7 @@ func (x *actorSystem) enableRemoting(ctx context.Context) {
 	// add some logging information
 	x.logger.Info("enabling remoting...")
 	// create a function to handle the observability
-	interceptor := func(tp trace.TracerProvider, mp metric.MeterProvider) connect.Interceptor {
+	interceptor := func(tp trace.TracerProvider, mp otelmetric.MeterProvider) connect.Interceptor {
 		return otelconnect.NewInterceptor(
 			otelconnect.WithTracerProvider(tp),
 			otelconnect.WithMeterProvider(mp),
@@ -1281,3 +1300,23 @@ func (x *actorSystem) housekeeper() {
 	// add some logging
 	x.logger.Info("Housekeeping has stopped...")
 }
+
+// registerMetrics register the PID metrics with OTel instrumentation.
+func (x *actorSystem) registerMetrics() error {
+	// grab the OTel meter
+	meter := x.telemetry.Meter
+	// create an instance of the ActorMetrics
+	metrics, err := metric.NewActorSystemMetric(meter)
+	// handle the error
+	if err != nil {
+		return err
+	}
+
+	// register the metrics
+	_, err = meter.RegisterCallback(func(ctx context.Context, observer otelmetric.Observer) error {
+		observer.ObserveInt64(metrics.ActorsCount(), int64(x.NumActors()))
+		return nil
+	}, metrics.ActorsCount())
+
+	return err
+}
diff --git a/actors/option.go b/actors/option.go
index 3f152a1..ca2f965 100644
--- a/actors/option.go
+++ b/actors/option.go
@@ -172,3 +172,10 @@ func WithTracing() Option {
 		system.traceEnabled.Store(true)
 	})
 }
+
+// WithMetric enables metrics
+func WithMetric() Option {
+	return OptionFunc(func(system *actorSystem) {
+		system.metricEnabled.Store(true)
+	})
+}
diff --git a/actors/pid.go b/actors/pid.go
index 1690e57..67bd324 100644
--- a/actors/pid.go
+++ b/actors/pid.go
@@ -39,6 +39,7 @@ import (
 	internalpb "github.com/tochemey/goakt/internal/v1"
 	"github.com/tochemey/goakt/internal/v1/internalpbconnect"
 	"github.com/tochemey/goakt/log"
+	"github.com/tochemey/goakt/metric"
 	addresspb "github.com/tochemey/goakt/pb/address/v1"
 	eventspb "github.com/tochemey/goakt/pb/events/v1"
 	messagespb "github.com/tochemey/goakt/pb/messages/v1"
@@ -46,9 +47,10 @@ import (
 	"github.com/tochemey/goakt/pkg/slices"
 	"github.com/tochemey/goakt/pkg/types"
 	"github.com/tochemey/goakt/telemetry"
-	"go.opentelemetry.io/otel"
 	"go.opentelemetry.io/otel/codes"
+	otelmetric "go.opentelemetry.io/otel/metric"
 	"go.opentelemetry.io/otel/trace"
+	"go.opentelemetry.io/otel/trace/noop"
 	"go.uber.org/atomic"
 	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/types/known/anypb"
@@ -148,7 +150,7 @@ type PID interface {
 	// unstash unstashes the oldest message in the stash and prepends to the mailbox
 	unstash() error
 	// toDeadletters add the given message to the deadletters queue
-	emitDeadletter(recvCtx ReceiveContext, err error)
+	emitDeadletter(receiveCtx ReceiveContext, err error)
 	// removeChild is a utility function to remove child actor
 	removeChild(pid PID)
 }
@@ -238,6 +240,11 @@ type pid struct {
 	// define tracing
 	traceEnabled atomic.Bool
 	tracer       trace.Tracer
+
+	// set the metric settings
+	restartCount  *atomic.Int64
+	childrenCount *atomic.Int64
+	metricEnabled atomic.Bool
 }
 
 // enforce compilation error
@@ -265,7 +272,9 @@ func newPID(ctx context.Context, actorPath *Path, actor Actor, opts ...pidOption
 		stashBuffer:        nil,
 		stashSemaphore:     sync.Mutex{},
 		eventsStream:       nil,
-		tracer:             trace.NewNoopTracerProvider().Tracer("PID"),
+		tracer:             noop.NewTracerProvider().Tracer("PID"),
+		restartCount:       atomic.NewInt64(0),
+		childrenCount:      atomic.NewInt64(0),
 	}
 
 	// set some of the defaults values
@@ -278,6 +287,7 @@ func newPID(ctx context.Context, actorPath *Path, actor Actor, opts ...pidOption
 	pid.replyTimeout.Store(DefaultReplyTimeout)
 	pid.initTimeout.Store(DefaultInitTimeout)
 	pid.traceEnabled.Store(false)
+	pid.metricEnabled.Store(false)
 
 	// set the custom options to override the default values
 	for _, opt := range opts {
@@ -300,7 +310,7 @@ func newPID(ctx context.Context, actorPath *Path, actor Actor, opts ...pidOption
 
 	// set the tracer when tracing is enabled
 	if pid.traceEnabled.Load() {
-		pid.tracer = otel.GetTracerProvider().Tracer("PID")
+		pid.tracer = pid.telemetry.Tracer
 	}
 
 	// initialize the actor and init processing public
@@ -314,6 +324,14 @@ func newPID(ctx context.Context, actorPath *Path, actor Actor, opts ...pidOption
 		go pid.passivationListener()
 	}
 
+	// register metrics. However, we don't panic when we fail to register
+	if pid.metricEnabled.Load() {
+		if err := pid.registerMetrics(); err != nil {
+			// return the error
+			return nil, errors.Wrapf(err, "failed to register actor=%s metrics", pid.ActorPath().String())
+		}
+	}
+
 	// return the actor reference
 	return pid, nil
 }
@@ -335,6 +353,9 @@ func (p *pid) Child(name string) (PID, error) {
 
 	// check whether the child actor already exist and just return the PID
 	if cid, ok := p.children.Get(childActorPath); ok {
+		// increment the children count
+		p.childrenCount.Inc()
+		// return the child PID
 		return cid, nil
 	}
 	return nil, ErrActorNotFound(childActorPath.String())
@@ -498,6 +519,9 @@ func (p *pid) Restart(ctx context.Context) error {
 
 	// successful restart
 	span.SetStatus(codes.Ok, "Restart")
+	// increment the restart count
+	p.restartCount.Inc()
+	// return
 	return nil
 }
 
@@ -559,6 +583,11 @@ func (p *pid) SpawnChild(ctx context.Context, name string, actor Actor) (PID, er
 		opts = append(opts, withTracing())
 	}
 
+	// set the pid metric option
+	if p.metricEnabled.Load() {
+		opts = append(opts, withMetric())
+	}
+
 	// create the child pid
 	cid, err := newPID(spanCtx,
 		childActorPath,
@@ -588,6 +617,11 @@ func (p *pid) SpawnChild(ctx context.Context, name string, actor Actor) (PID, er
 
 // StashSize returns the stash buffer size
 func (p *pid) StashSize() uint64 {
+	// avoid panic in case stash is not enabled
+	if p.stashBuffer == nil {
+		return 0
+	}
+	// return the stash buffer size
 	return p.stashBuffer.Size()
 }
 
@@ -1138,6 +1172,13 @@ func (p *pid) reset() {
 	p.mailbox.Reset()
 	// reset the behavior stack
 	p.resetBehavior()
+	// re-register metric when metric is enabled
+	if p.metricEnabled.Load() {
+		// log the error but don't panic
+		if err := p.registerMetrics(); err != nil {
+			p.logger.Error(errors.Wrapf(err, "failed to register actor=%s metrics", p.ActorPath().String()))
+		}
+	}
 }
 
 func (p *pid) freeWatchers(ctx context.Context) {
@@ -1468,17 +1509,17 @@ func (p *pid) removeChild(pid PID) {
 }
 
 // emitDeadletter emit the given message to the deadletters queue
-func (p *pid) emitDeadletter(recvCtx ReceiveContext, err error) {
+func (p *pid) emitDeadletter(receiveCtx ReceiveContext, err error) {
 	// only send to the stream when defined
 	if p.eventsStream == nil {
 		return
 	}
 	// marshal the message
-	msg, _ := anypb.New(recvCtx.Message())
+	msg, _ := anypb.New(receiveCtx.Message())
 	// grab the sender
 	var senderAddr *addresspb.Address
-	if recvCtx.Sender() != nil || recvCtx.Sender() != NoSender {
-		senderAddr = recvCtx.Sender().ActorPath().RemoteAddress()
+	if receiveCtx.Sender() != nil || receiveCtx.Sender() != NoSender {
+		senderAddr = receiveCtx.Sender().ActorPath().RemoteAddress()
 	}
 
 	// define the receiver
@@ -1494,3 +1535,27 @@ func (p *pid) emitDeadletter(recvCtx ReceiveContext, err error) {
 	// add to the events stream
 	p.eventsStream.Publish(deadlettersTopic, deadletter)
 }
+
+// registerMetrics register the PID metrics with OTel instrumentation.
+func (p *pid) registerMetrics() error {
+	// grab the OTel meter
+	meter := p.telemetry.Meter
+	// create an instance of the ActorMetrics
+	metrics, err := metric.NewActorMetric(meter)
+	// handle the error
+	if err != nil {
+		return err
+	}
+
+	// register the metrics
+	_, err = meter.RegisterCallback(func(ctx context.Context, observer otelmetric.Observer) error {
+		observer.ObserveInt64(metrics.ChildrenCount(), p.childrenCount.Load())
+		observer.ObserveInt64(metrics.StashCount(), int64(p.StashSize()))
+		observer.ObserveInt64(metrics.RestartCount(), p.restartCount.Load())
+		return nil
+	}, metrics.ChildrenCount(),
+		metrics.StashCount(),
+		metrics.RestartCount())
+
+	return err
+}
diff --git a/actors/pid_option.go b/actors/pid_option.go
index 8775594..c3954f9 100644
--- a/actors/pid_option.go
+++ b/actors/pid_option.go
@@ -141,3 +141,9 @@ func withTracing() pidOption {
 		pid.traceEnabled.Store(true)
 	}
 }
+
+func withMetric() pidOption {
+	return func(pid *pid) {
+		pid.metricEnabled.Store(true)
+	}
+}
diff --git a/metric/actor_metric.go b/metric/actor_metric.go
new file mode 100644
index 0000000..757dbc8
--- /dev/null
+++ b/metric/actor_metric.go
@@ -0,0 +1,118 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022-2023 Tochemey
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package metric
+
+import (
+	"fmt"
+
+	"go.opentelemetry.io/otel/metric"
+)
+
+// ActorMetric defines the actor instrumentation
+type ActorMetric struct {
+	// Specifies the total number of child actors
+	childrenCount metric.Int64ObservableCounter
+	// Specifies the total number of stashed messages
+	stashCount metric.Int64ObservableCounter
+	// Specifies the total number of restarts
+	restartCount metric.Int64ObservableCounter
+	// Specifies the total number of instance created
+	spawnCount metric.Int64ObservableCounter
+	// Specifies the last message received processing duration
+	// This is expressed in milliseconds
+	lastReceivedDuration metric.Float64Histogram
+}
+
+// NewActorMetric creates an instance of ActorMetric
+func NewActorMetric(meter metric.Meter) (*ActorMetric, error) {
+	// create an instance of ActorMetric
+	actorMetric := new(ActorMetric)
+	var err error
+	// set the child count instrument
+	if actorMetric.childrenCount, err = meter.Int64ObservableCounter(
+		"actor_child_count",
+		metric.WithDescription("Total number of child actors"),
+	); err != nil {
+		return nil, fmt.Errorf("failed to create childrenCount instrument, %v", err)
+	}
+	// set the stashed messages count instrument
+	if actorMetric.stashCount, err = meter.Int64ObservableCounter(
+		"actor_stash_count",
+		metric.WithDescription("Total number of messages stashed"),
+	); err != nil {
+		return nil, fmt.Errorf("failed to create stashCount instrument, %v", err)
+	}
+
+	// set the spawn messages count instrument
+	if actorMetric.spawnCount, err = meter.Int64ObservableCounter(
+		"actor_spawn_count",
+		metric.WithDescription("Total number of instances created"),
+	); err != nil {
+		return nil, fmt.Errorf("failed to create spawnCount instrument, %v", err)
+	}
+
+	// set the spawn messages count instrument
+	if actorMetric.restartCount, err = meter.Int64ObservableCounter(
+		"actor_restart_count",
+		metric.WithDescription("Total number of restart"),
+	); err != nil {
+		return nil, fmt.Errorf("failed to create restartCount instrument, %v", err)
+	}
+
+	if actorMetric.lastReceivedDuration, err = meter.Float64Histogram(
+		"actor_received_duration",
+		metric.WithDescription("The latency of the last message processed in milliseconds"),
+		metric.WithUnit("ms"),
+	); err != nil {
+		return nil, fmt.Errorf("failed to create lastReceivedDuration instrument, %v", err)
+	}
+
+	return actorMetric, nil
+}
+
+// ChildrenCount returns the total number of child actors
+func (x *ActorMetric) ChildrenCount() metric.Int64ObservableCounter {
+	return x.childrenCount
+}
+
+// StashCount returns the total number of stashed messages
+func (x *ActorMetric) StashCount() metric.Int64ObservableCounter {
+	return x.stashCount
+}
+
+// RestartCount returns the total number of restart
+func (x *ActorMetric) RestartCount() metric.Int64ObservableCounter {
+	return x.restartCount
+}
+
+// SpawnCount returns the total number of instances created for the given actor
+func (x *ActorMetric) SpawnCount() metric.Int64ObservableCounter {
+	return x.spawnCount
+}
+
+// LastReceivedDuration returns the last message received duration latency in milliseconds
+func (x *ActorMetric) LastReceivedDuration() metric.Float64Histogram {
+	return x.lastReceivedDuration
+}
diff --git a/metric/system_metric.go b/metric/system_metric.go
new file mode 100644
index 0000000..1835264
--- /dev/null
+++ b/metric/system_metric.go
@@ -0,0 +1,69 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022-2023 Tochemey
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package metric
+
+import (
+	"fmt"
+
+	"go.opentelemetry.io/otel/metric"
+)
+
+// ActorSystemMetric defines the actor system metrics
+type ActorSystemMetric struct {
+	actorsCount     metric.Int64ObservableCounter
+	deadletterCount metric.Int64ObservableCounter
+}
+
+// NewActorSystemMetric creates an instance of ActorSystemMetric
+func NewActorSystemMetric(meter metric.Meter) (*ActorSystemMetric, error) {
+	// create an instance of ActorMetric
+	systemMetric := new(ActorSystemMetric)
+	var err error
+	// set the child count instrument
+	if systemMetric.actorsCount, err = meter.Int64ObservableCounter(
+		"actors_count",
+		metric.WithDescription("Total number of actors"),
+	); err != nil {
+		return nil, fmt.Errorf("failed to create actorsCount instrument, %v", err)
+	}
+	// set the stashed messages count instrument
+	if systemMetric.deadletterCount, err = meter.Int64ObservableCounter(
+		"actor_system_deadletter_count",
+		metric.WithDescription("Total number of deadletter messages"),
+	); err != nil {
+		return nil, fmt.Errorf("failed to create deadletterCount instrument, %v", err)
+	}
+	return systemMetric, nil
+}
+
+// ActorsCount returns the total number of actors
+func (x *ActorSystemMetric) ActorsCount() metric.Int64ObservableCounter {
+	return x.actorsCount
+}
+
+// DeadletterCount returns the total number of deadletter
+func (x *ActorSystemMetric) DeadletterCount() metric.Int64ObservableCounter {
+	return x.deadletterCount
+}
diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go
index 033978a..84e5046 100644
--- a/telemetry/telemetry.go
+++ b/telemetry/telemetry.go
@@ -31,13 +31,7 @@ import (
 )
 
 const (
-	instrumentationName           = "github.com/Tochemey/goakt"
-	failureCounterName            = "actor_failure_count"
-	receivedCounterName           = "actor_received_count"
-	mailboxGaugeName              = "actor_mailbox_gauge"
-	startedCounterName            = "actor_start_count"
-	receivedDurationHistogramName = "actor_received_duration"
-	actorSystemActorsCounterName  = "actor_system_actors_count"
+	instrumentationName = "github.com/Tochemey/goakt"
 )
 
 // Telemetry encapsulates some settings for an actor


Test patch:
diff --git a/actors/actor_system_test.go b/actors/actor_system_test.go
index dca85fe..221ffcd 100644
--- a/actors/actor_system_test.go
+++ b/actors/actor_system_test.go
@@ -39,9 +39,12 @@ import (
 	"github.com/tochemey/goakt/log"
 	addresspb "github.com/tochemey/goakt/pb/address/v1"
 	eventspb "github.com/tochemey/goakt/pb/events/v1"
+	"github.com/tochemey/goakt/telemetry"
 	testpb "github.com/tochemey/goakt/test/data/pb/v1"
 	testkit "github.com/tochemey/goakt/testkit/discovery"
 	"github.com/travisjeffery/go-dynaport"
+	sdkmetric "go.opentelemetry.io/otel/sdk/metric"
+	"go.opentelemetry.io/otel/sdk/metric/metricdata"
 	"google.golang.org/protobuf/proto"
 )
 
@@ -874,7 +877,7 @@ func TestActorSystem(t *testing.T) {
 		require.NoError(t, err)
 		require.NotNil(t, consumer)
 
-		// create the blackhole actor
+		// create the black hole actor
 		actor := &BlackHole{}
 		actorRef, err := sys.Spawn(ctx, "BlackHole ", actor)
 		assert.NoError(t, err)
@@ -1023,4 +1026,48 @@ func TestActorSystem(t *testing.T) {
 			provider.AssertExpectations(t)
 		})
 	})
+	t.Run("With Metric enabled", func(t *testing.T) {
+		r := sdkmetric.NewManualReader()
+		mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(r))
+		// create an instance of telemetry
+		tel := telemetry.New(telemetry.WithMeterProvider(mp))
+
+		ctx := context.TODO()
+		sys, _ := NewActorSystem("testSys",
+			WithMetric(),
+			WithTelemetry(tel),
+			WithStash(5),
+			WithLogger(log.DiscardLogger))
+
+		// start the actor system
+		err := sys.Start(ctx)
+		assert.NoError(t, err)
+
+		actor := NewTester()
+		actorRef, err := sys.Spawn(ctx, "Test", actor)
+		assert.NoError(t, err)
+		assert.NotNil(t, actorRef)
+
+		// create a message to send to the test actor
+		message := new(testpb.TestSend)
+		// send the message to the actor
+		err = Tell(ctx, actorRef, message)
+		// perform some assertions
+		require.NoError(t, err)
+
+		// Should collect 4 metrics, 3 for the actor and 1 for the actor system
+		got := &metricdata.ResourceMetrics{}
+		err = r.Collect(ctx, got)
+		require.NoError(t, err)
+		assert.Len(t, got.ScopeMetrics, 1)
+		assert.Len(t, got.ScopeMetrics[0].Metrics, 4)
+
+		// stop the actor after some time
+		time.Sleep(time.Second)
+
+		t.Cleanup(func() {
+			err = sys.Stop(ctx)
+			assert.NoError(t, err)
+		})
+	})
 }
diff --git a/actors/option_test.go b/actors/option_test.go
index 67aa861..eaa0dcf 100644
--- a/actors/option_test.go
+++ b/actors/option_test.go
@@ -121,6 +121,11 @@ func TestOptions(t *testing.T) {
 			option:   WithTracing(),
 			expected: actorSystem{traceEnabled: atomicTrue},
 		},
+		{
+			name:     "WithMetric",
+			option:   WithMetric(),
+			expected: actorSystem{metricEnabled: atomicTrue},
+		},
 	}
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
diff --git a/actors/pid_option_test.go b/actors/pid_option_test.go
index 6581b13..be17b26 100644
--- a/actors/pid_option_test.go
+++ b/actors/pid_option_test.go
@@ -120,6 +120,11 @@ func TestPIDOptions(t *testing.T) {
 			option:   withTracing(),
 			expected: &pid{traceEnabled: atomicTrue},
 		},
+		{
+			name:     "withMetric",
+			option:   withMetric(),
+			expected: &pid{metricEnabled: atomicTrue},
+		},
 	}
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
diff --git a/actors/pid_test.go b/actors/pid_test.go
index 688c240..dc19712 100644
--- a/actors/pid_test.go
+++ b/actors/pid_test.go
@@ -31,14 +31,16 @@ import (
 	"testing"
 	"time"
 
-	"go.opentelemetry.io/otel/trace"
-
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/tochemey/goakt/log"
 	messagespb "github.com/tochemey/goakt/pb/messages/v1"
+	"github.com/tochemey/goakt/telemetry"
 	testpb "github.com/tochemey/goakt/test/data/pb/v1"
 	"github.com/travisjeffery/go-dynaport"
+	sdkmetric "go.opentelemetry.io/otel/sdk/metric"
+	"go.opentelemetry.io/otel/sdk/metric/metricdata"
+	"go.opentelemetry.io/otel/trace"
 	"google.golang.org/protobuf/proto"
 )
 
@@ -68,7 +70,7 @@ func TestActorReceive(t *testing.T) {
 	// let us send 10 public to the actor
 	count := 10
 	for i := 0; i < count; i++ {
-		recvContext := &receiveContext{
+		receiveContext := &receiveContext{
 			ctx:            ctx,
 			message:        new(testpb.TestSend),
 			sender:         NoSender,
@@ -77,7 +79,7 @@ func TestActorReceive(t *testing.T) {
 			isAsyncMessage: true,
 		}
 
-		pid.doReceive(recvContext)
+		pid.doReceive(receiveContext)
 	}
 
 	// stop the actor
@@ -1357,3 +1359,56 @@ func TestBatchAsk(t *testing.T) {
 		assert.NoError(t, pid.Shutdown(ctx))
 	})
 }
+func TestRegisterMetrics(t *testing.T) {
+	r := sdkmetric.NewManualReader()
+	mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(r))
+	// create an instance of telemetry
+	tel := telemetry.New(telemetry.WithMeterProvider(mp))
+
+	ctx := context.TODO()
+
+	// create the actor path
+	actorPath := NewPath("Test", NewAddress("sys", "host", 1))
+
+	// create the actor ref
+	pid, err := newPID(
+		ctx,
+		actorPath,
+		NewTester(),
+		withInitMaxRetries(1),
+		withCustomLogger(log.DefaultLogger),
+		withTelemetry(tel),
+		withMetric(),
+		withSendReplyTimeout(receivingTimeout))
+
+	require.NoError(t, err)
+	assert.NotNil(t, pid)
+
+	// let us send 10 public to the actor
+	count := 10
+	for i := 0; i < count; i++ {
+		receiveContext := &receiveContext{
+			ctx:            ctx,
+			message:        new(testpb.TestSend),
+			sender:         NoSender,
+			recipient:      pid,
+			mu:             sync.Mutex{},
+			isAsyncMessage: true,
+		}
+
+		pid.doReceive(receiveContext)
+	}
+
+	// assert some metrics
+
+	// Should collect 3 metrics
+	got := &metricdata.ResourceMetrics{}
+	err = r.Collect(ctx, got)
+	require.NoError(t, err)
+	assert.Len(t, got.ScopeMetrics, 1)
+	assert.Len(t, got.ScopeMetrics[0].Metrics, 3)
+
+	// stop the actor
+	err = pid.Shutdown(ctx)
+	assert.NoError(t, err)
+}
diff --git a/metric/actor_metric_test.go b/metric/actor_metric_test.go
new file mode 100644
index 0000000..3512542
--- /dev/null
+++ b/metric/actor_metric_test.go
@@ -0,0 +1,47 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022-2023 Tochemey
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package metric
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"go.opentelemetry.io/otel/metric/noop"
+)
+
+func TestNewActorMetric(t *testing.T) {
+	// create the instance of a meter
+	meter := noop.NewMeterProvider().Meter("test")
+	// create an instance of Actor metric
+	actorMetric, err := NewActorMetric(meter)
+	require.NoError(t, err)
+	assert.NotNil(t, actorMetric)
+	assert.NotNil(t, actorMetric.RestartCount())
+	assert.NotNil(t, actorMetric.StashCount())
+	assert.NotNil(t, actorMetric.SpawnCount())
+	assert.NotNil(t, actorMetric.LastReceivedDuration())
+	assert.NotNil(t, actorMetric.ChildrenCount())
+}
diff --git a/metric/system_metric_test.go b/metric/system_metric_test.go
new file mode 100644
index 0000000..dc1babe
--- /dev/null
+++ b/metric/system_metric_test.go
@@ -0,0 +1,44 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022-2023 Tochemey
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package metric
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"go.opentelemetry.io/otel/metric/noop"
+)
+
+func TestNewActorSystemMetric(t *testing.T) {
+	// create the instance of a meter
+	meter := noop.NewMeterProvider().Meter("test")
+	// create an instance of Actor metric
+	systemMetric, err := NewActorSystemMetric(meter)
+	require.NoError(t, err)
+	assert.NotNil(t, systemMetric)
+	assert.NotNil(t, systemMetric.DeadletterCount())
+	assert.NotNil(t, systemMetric.ActorsCount())
+}