Extend app custom metrics
Overview
Extend app custom metrics are additional metrics other than the default metrics exposed by Extend apps.
This article guides you through the process of adding a custom metric to your Extend app, using the Extend Service Extension app template as an example. However, the steps outlined apply to all types of Extend apps.
Prerequisites
Before following this walkthrough, you should be familiar with how to set up an Extend Service Extension app.
Clone the app template
- C#
- Go
- Java
- Python
git clone https://github.com/AccelByte/extend-service-extension-csharp
git clone https://github.com/AccelByte/extend-service-extension-go
git clone https://github.com/AccelByte/extend-service-extension-java
git clone https://github.com/AccelByte/extend-service-extension-python
Add a custom metric
In this example, we will add a counter metric that tracks the number of times the GetGuildProgress function is called.
- C#
- Go
- Java
- Python
-
Define the new metric by creating a metric provider class. Save it inside
Classesdirectory or any other directory.using System;
using Prometheus;
namespace AccelByte.Extend.ServiceExtension.Server
{
public class GetGuildCountMetric
{
private Counter _Counter;
public GetGuildCountMetric()
{
_Counter = Metrics.CreateCounter(
"ab_count_get_guild_progress",
"Count number of GetGuildProgress called", labelNames: new[] { "method" });
}
public void Increment(string method)
{
_Counter.WithLabels(method).Inc();
}
}
} -
Register the new metric in service provider inside
Program.csfile. Put code below before registering any grpc services....
builder.Services
.AddSingleton(new GetGuildCountMetric());
... -
Add the new metric inside grpc service constructor in
Services/MyService.cs....
public class MyService : Service.ServiceBase
{
...
private readonly GetGuildCountMetric _GetGuildCountMetric;
...
public MyService(
ILogger<MyService> logger,
IAccelByteServiceProvider abProvider,
Tracer tracer,
GetGuildCountMetric getGuildCountMetric)
{
...
_GetGuildCountMetric = getGuildCountMetric;
}
...
} -
In the
GetGuildProgressfunction, increment the metrics counter each time the function is called._GetGuildCountMetric.Increment("GET", context.Method);
-
Define the new metric by creating a Prometheus
CounterVecand storing it incounterGetGuildProgressvariable inmain.go. The metric will include one label:method.var (
serviceName = common.GetEnv("OTEL_SERVICE_NAME", "ExtendCustomServiceGoDocker")
logLevelStr = common.GetEnv("LOG_LEVEL", logrus.InfoLevel.String())
// The new variable
counterGetGuildProgress = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "ab_count_get_guild_progress",
Help: "Count number of GetGuildProgress called",
},
[]string{"method"},
)
) -
Register the
counterGetGuildProgressvariable with the Prometheus registry in main.go.// Register Prometheus metrics
prometheusRegistry := prometheus.NewRegistry()
prometheusRegistry.MustRegister(
prometheusCollectors.NewGoCollector(),
prometheusCollectors.NewProcessCollector(prometheusCollectors.ProcessCollectorOpts{}),
prometheusGrpc.DefaultServerMetrics,
// The new custom metric
counterGetGuildProgress,
) -
Pass the
counterGetGuildProgressvariable to theGetGuildProgressfunction. Add a new field to theMyServiceServerImplstruct inpkg/service/myService.goto hold the custom metric. Additionally, update the constructor to initializecounterGetGuildProgresswhen creating a new instance ofMyServiceServerImpl.type MyServiceServerImpl struct {
pb.UnimplementedServiceServer
tokenRepo repository.TokenRepository
configRepo repository.ConfigRepository
refreshRepo repository.RefreshTokenRepository
storage storage.Storage
counterGetGuildProgress prometheus.CounterVec // New field
}
func NewMyServiceServer(
tokenRepo repository.TokenRepository,
configRepo repository.ConfigRepository,
refreshRepo repository.RefreshTokenRepository,
storage storage.Storage,
counterGetGuildProgress prometheus.CounterVec, // Newly added
) *MyServiceServerImpl {
return &MyServiceServerImpl{
tokenRepo: tokenRepo,
configRepo: configRepo,
refreshRepo: refreshRepo,
storage: storage,
counterGetGuildProgress: counterGetGuildProgress, // Newly added
}
} -
In the
GetGuildProgressfunction, increment the metrics counter each time the function is called.func (g MyServiceServerImpl) GetGuildProgress(
ctx context.Context, req *pb.GetGuildProgressRequest,
) (*pb.GetGuildProgressResponse, error) {
namespace := req.Namespace
guildProgressKey := fmt.Sprintf("guildProgress_%s", req.GuildId)
// called the custom metric
g.counterGetGuildProgress.WithLabelValues("GET").Inc()
guildProgress, err := g.storage.GetGuildProgress(namespace, guildProgressKey)
if err != nil {
return nil, status.Errorf(codes.Internal, "Error getting guild progress: %v", err)
}
return &pb.GetGuildProgressResponse{
GuildProgress: guildProgress,
}, nil
}
-
Add the
spring-aspectsdependency tobuild.gradlefile.dependencies {
...
implementation 'org.springframework:spring-aspects:6.1.4'
...
testImplementation 'org.springframework:spring-aspects:6.1.4'
} -
Add
CountedAspecttosrc/main/java/net/accelbyte/extend/serviceextension/config/AppConfig.javafile....
import io.micrometer.core.aop.CountedAspect;
import io.micrometer.core.instrument.MeterRegistry;
@Configuration
public class AppConfig {
...
@Bean
public CountedAspect provideCounterAspect(MeterRegistry registry) {
return new CountedAspect(registry);
}
} -
Add
@Countedannotation togetGuildProgressmethod insrc/main/java/net/accelbyte/extend/serviceextension/service/MyService.javafile....
import io.micrometer.core.annotation.Counted;
@GRpcService
@Slf4j
public class MyService extends ServiceGrpc.ServiceImplBase {
@Counted(value = "ab_count_get_guild_progress",
description = "Count number of GetGuildProgress called",
extraTags = { "method", "GET" })
@Override
public void getGuildProgress(
GetGuildProgressRequest request, StreamObserver<GetGuildProgressResponse> responseObserver
) {
...
}
}
-
Define the new metric by creating a Prometheus
Counterand storing it incounter_get_guild_progressvariable in your service's.pyfile. The metric will include one label:method.from prometheus_client import Counter
class AsyncMyService(MyServiceServices):
def __init__(self, *args, **kwargs) -> None:
self.counter_get_guild_progress = Counter(
name="ab_count_get_guild_progress",
documentation="Count number of GetGuildProgress called",
labelnames=["method"],
unit="count",
) -
In the
GetGuildProgressfunction, increment the metrics counter each time the function is called.async def GetGuildProgress(
self, request: GetGuildProgressRequest, context: Any
) -> GetGuildProgressResponse:
# ...
self.counter_get_guild_progress.labels(method="GET").inc(amount=1)
# ...
Explore metrics in Grafana Cloud
-
Access the Grafana Cloud by clicking the
Open Grafana Cloudbutton on your Extend app details page in the Admin Portal.. -
Click the
Exploremenu in the Grafana Cloud.
-
Select a data source to load the metrics data.
There are two Prometheus data sources that you can select depending on your account permissions in Admin Portal. If you have access to all namespaces (super admin), you can select
metricsdata source (cross-namespace data). If you only have access to a specific namespace, you can only selectprom-<namespace>.*data source (per-namespace data).
Cross-namespace data 
Per-namespace data -
Add or remove multiple queries as needed. Once the source is selected, click the Add query button. Additionally, Grafana allows you to view query history by clicking the Query history button.

-
To select a custom metric, use the
Select metricdropdown and search for your custom metric. You can also click theOperationsbutton to apply a specific operation.
If you have a complex query, click the
Codetab to use Prometheus queries for advanced calculations.
Follow the steps in the image below: First, select the source (1). Next, choose the time frame for the query (2). Then, select values from the
Metricdrop down box andLabelfilters (3). Optionally, apply a specific operation to your metrics (4). Finally, click theRun querybutton to view the metric.
Important notes
-
A series is defined as a unique combination of a metric name and a set of label key-value pairs. Each distinct metric name, along with its corresponding label key-value pairs, is treated as a separate series. For example:
ab_count_get_guild_progress{method="GET"} 1 -> series_count=1
ab_count_get_guild_progress{method="GET"} 5 -> series_count=1
ab_count_get_guild_progress{method="POST"} 5 -> series_count=2
ab_sum_get_guild_progress{method="GET", service="guild"} 1 -> series_count=3 -
Custom metrics are limited to a maximum of 1,000 active series per environment. Once this limit is reached, no new series can be pushed.
a. A metric is considered an active series if it is consistently scraped and received by Grafana Cloud.
b. A metric is considered a non-active series if it is not scraped or received by Grafana Cloud for 20 minutes.
-
Currently, it is not possible to create custom dashboard to display metrics.
-
Extend app metrics, including custom metrics, are published at the
/metricsendpoint on port 8080. This configuration is set up by default when using the Extend app templates to create your Extend app. -
The table below shows the reserved labels that cannot be used in custom metrics. If any of these are used, they will be replaced with the predefined values.
Label Key Value Description environment_name ${env_name}Environment identifier name product extend Product identifier name: extend scenario ${extend_app_type}Extend app type: function-override,event-handler, orservice-extensionunion_namespace ${publisher_name}Publisher name game_namespace ${game_namespace}Extend app namespace app_name ${app_name}Extend app name job metric-services/extend-metrics Predefined job name instance extend-metric-instance Predefined instance value
Add a unique prefix to your custom metric to easily recognize it and differentiate it from default metrics. For example, ab_count_get_guild_progress, where ab_ serves as the unique identifier prefix.