Grafana — Interfaces#
Sampling note#
Grafana has 1,036 interface definitions across its non-vendor Go files. This analysis focuses on the 5 most architecturally significant files, selected based on the architecture result’s core components: lifecycle management (pkg/registry/), plugin system (pkg/plugins/), infrastructure layer (pkg/infra/), domain services (pkg/services/dashboards/), and HTTP routing (pkg/api/routing/). Proto-generated interfaces (pb.go) are excluded.
Interface catalog#
BackgroundService#
- Package:
github.com/grafana/grafana/pkg/registry - File:
pkg/registry/registry.go:25 - Methods:
Run(ctx context.Context) error - Purpose: Universal lifecycle contract for every long-running service in Grafana. Any service that does async background work implements this interface. The
ManagerAdapter(wrappinggrafana/dskit’sModuleManager) discovers all registeredBackgroundServiceinstances and starts them concurrently after the Init phase. - Implementations: ~60 implementations across
pkg/services/— every service with a background loop (HTTP server, alerting scheduler, provisioning poller, plugin loader, stats collector, etc.) - Design quality: Exemplary ISP. Single-method interface, maximally composable. The
CanBeDisabledinterface (also single-method:IsDisabled() bool) is an optional refinement checked via type assertion. This demonstrates Go’s implicit interface satisfaction — services opt into optionality without changing the base contract.
BackgroundServiceRegistry / CanBeDisabled#
- Package:
github.com/grafana/grafana/pkg/registry - File:
pkg/registry/registry.go:10 - Methods (BackgroundServiceRegistry):
GetServices() []BackgroundService - Methods (CanBeDisabled):
IsDisabled() bool - Purpose:
BackgroundServiceRegistryis the collection interface through which the module manager discovers all services.CanBeDisabledis an optional “narrowing” interface: the framework callsIsDisabled()(via type assertion) after Init to decide whether to skip starting a service. - Implementations:
BackgroundServiceRegistry— one concrete implementation wired via Wire DI.CanBeDisabled— e.g., alerting service, which disables itself when the feature is turned off. - Design quality: Good use of optional interface enrichment via type assertion rather than making every service implement
IsDisabled. Keeps the primary contract minimal.
DashboardService (representative domain service interface)#
- Package:
github.com/grafana/grafana/pkg/services/dashboards - File:
pkg/services/dashboards/dashboard.go:21 - Methods:
BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, validateProvisionedDashboard bool) (*SaveDashboardCommand, error) DeleteDashboard(ctx context.Context, dashboardId int64, dashboardUID string, orgId int64) error DeleteAllDashboards(ctx context.Context, orgID int64) error FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) GetDashboards(ctx context.Context, query *GetDashboardsQuery) ([]*Dashboard, error) GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) (*DashboardRef, error) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*Dashboard, error) SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*Dashboard, error) SearchDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) (model.HitList, error) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) (int64, error) GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*Dashboard, error) CleanUpDashboard(ctx context.Context, dashboardUID string, dashboardId int64, orgId int64) error CountDashboardsInOrg(ctx context.Context, orgID int64) (int64, error) SetDefaultPermissions(ctx context.Context, dto *SaveDashboardDTO, dash *Dashboard, provisioned bool) UnstructuredToLegacyDashboard(ctx context.Context, item *unstructured.Unstructured, orgID int64) (*Dashboard, error) ValidateDashboardRefreshInterval(minRefreshInterval string, targetRefreshInterval string) error ValidateBasicDashboardProperties(title string, uid string, message string) error GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*DashboardRef, error) - Purpose: The full contract for the dashboard domain service layer. Separates business logic from the HTTP API handlers and from the storage layer (
Storeinterface). Consumer-side code (API handlers, provisioning) only depends on this interface, not on the implementation struct. - Implementations:
DashboardServiceImpl(main implementation). Mock generated bymockeryasFakeDashboardServicein the same package (//go:generate mockerydirective at the interface declaration). The same package also defines separate narrower interfaces:DashboardAccessService,PluginService,DashboardProvisioningService,Store. - Design quality: Somewhat broad (20 methods) — a God-interface for the dashboard domain. Mitigated by the fact that Grafana also provides narrower split interfaces (
DashboardAccessService,PluginService,DashboardProvisioningService) for consumers that only need a subset. TheStoreinterface separately abstracts persistence. The//go:generate mockerydirective is the standard pattern across all domain services in the project.
Store (dashboard persistence layer)#
- Package:
github.com/grafana/grafana/pkg/services/dashboards - File:
pkg/services/dashboards/dashboard.go:82 - Methods:
DeleteDashboard(ctx context.Context, cmd *DeleteDashboardCommand) error CleanupAfterDelete(ctx context.Context, cmd *DeleteDashboardCommand) error FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) GetDashboardsByPluginID(ctx context.Context, query *GetDashboardsByPluginIDQuery) ([]*Dashboard, error) // ... ~12 more methods SaveDashboard(ctx context.Context, cmd SaveDashboardCommand) (*Dashboard, error) ValidateDashboardBeforeSave(ctx context.Context, dashboard *Dashboard, overwrite bool) (bool, error) CountInOrg(ctx context.Context, orgID int64, isFolder bool) (int64, error) - Purpose: Abstracts the SQL persistence layer from the domain service.
DashboardServiceimplementations depend onStore, not onSQLStoredirectly. Enables testing the service layer with aFakeDashboardStorewithout a real database. - Implementations: Concrete SQL implementation (
dashboardStore). Mock:FakeDashboardStore(mockery-generated). - Design quality: Clean layering. The pattern of
Service interface+Store interfacein the same package is repeated across all ~60 service packages. This is Grafana’s standard architecture for the domain service layer.
DB (infrastructure database interface)#
- Package:
github.com/grafana/grafana/pkg/infra/db - File:
pkg/infra/db/db.go:18 - Methods:
WithTransactionalDbSession(ctx context.Context, callback sqlstore.DBTransactionFunc) error WithDbSession(ctx context.Context, callback sqlstore.DBTransactionFunc) error GetDialect() migrator.Dialect GetDBType() core.DbType GetEngine() *xorm.Engine GetSqlxSession() *session.SessionDB InTransaction(ctx context.Context, fn func(ctx context.Context) error) error Quote(value string) string RecursiveQueriesAreSupported() (bool, error) - Purpose: The cross-cutting database access abstraction used by all
Storeimplementations. Wrapsxorm.Engineand provides both the callback-based session model (WithDbSession) and the context-propagation transaction model (InTransaction). TheGetSqlxSession()is a forward-looking escape hatch towardsqlx. - Implementations:
sqlstore.SQLStore(concrete, wraps xorm). Test helpers (InitTestDB,SetupTestDB) return*SQLStoredirectly for integration tests. - Design quality: Pragmatic. Exposes
GetEngine()which leaks the xorm abstraction — a known trade-off for migration path toward sqlx. The dual session model (WithDbSessionvsInTransaction) reflects an in-progress migration toward context-based transaction propagation. Not a pure abstraction, but serviceable for a monolith transitioning storage layers.
PluginClient (plugin RPC contract)#
- Package:
github.com/grafana/grafana/pkg/plugins - File:
pkg/plugins/plugins.go:473 - Methods: (all embedded from
grafana-plugin-sdk-go/backend):// backend.QueryDataHandler: QueryData(ctx context.Context, req *QueryDataRequest) (*QueryDataResponse, error) // backend.QueryChunkedDataHandler: QueryDataStream(ctx context.Context, req *QueryDataRequest) (<-chan *QueryDataResponse, error) // backend.CollectMetricsHandler: CollectMetrics(ctx context.Context, req *CollectMetricsRequest) (*CollectMetricsResult, error) // backend.CheckHealthHandler: CheckHealth(ctx context.Context, req *CheckHealthRequest) (*CheckHealthResult, error) // backend.CallResourceHandler: CallResource(ctx context.Context, req *CallResourceRequest, sender CallResourceResponseSender) error // backend.AdmissionHandler, ConversionHandler, StreamHandler // ... (additional gRPC-mapped methods) - Purpose: The complete gRPC protocol contract between the Grafana host and backend plugin processes. Any code that queries a data source calls a
PluginClient. The interface aggregates all the protocol handlers that a plugin may implement. - Implementations: The concrete implementation is the gRPC client generated by
grafana-plugin-sdk-go. For built-in plugins that don’t run as separate processes, an in-process adapter implements the same interface. - Design quality: The interface is broad by design (8 embedded sub-interfaces), reflecting the full plugin SDK protocol. This is unavoidable given the multi-capability nature of plugins. Consumer code that only needs
QueryDatashould acceptbackend.QueryDataHandlerdirectly — but thePluginClientis the “whole plugin” contract.
PluginSource (plugin discovery)#
- Package:
github.com/grafana/grafana/pkg/plugins - File:
pkg/plugins/ifaces.go:20 - Methods:
PluginClass(ctx context.Context) Class DefaultSignature(ctx context.Context, pluginID string) (Signature, bool) Discover(ctx context.Context) ([]*FoundBundle, error) - Purpose: Abstraction over where plugins come from — local filesystem, CDN, or the Grafana plugin catalog API. The plugin loader iterates over all registered
PluginSourceinstances during startup discovery. - Implementations:
LocalSource(filesystem),CDNSource,GrafanaComSource(marketplace),AngularDetectorSource. - Design quality: Well-segregated 3-method interface. Follows ISP precisely. The
DefaultSignaturemethod handles the signature bootstrapping problem for unsigned core plugins without polluting other abstractions.
RouteRegister (HTTP routing contract)#
- Package:
github.com/grafana/grafana/pkg/api/routing - File:
pkg/api/routing/route_register.go:18 - Methods:
Get(string, ...web.Handler) Post(string, ...web.Handler) Delete(string, ...web.Handler) Put(string, ...web.Handler) Patch(string, ...web.Handler) Any(string, ...web.Handler) Group(string, func(RouteRegister), ...web.Handler) Insert(string, func(RouteRegister), ...web.Handler) Register(Router, ...RegisterNamedMiddleware) Reset() - Purpose: Allows any service or component to contribute HTTP routes to the server without depending on the concrete HTTP router. Individual route groups are registered by calling
Group()with afunc(RouteRegister)callback, enabling hierarchical route registration with prefix composition. - Implementations:
RouteRegisterImpl— a tree-structured route registry that serializes into aRouteron server start. Alsoweb.Macaron(the underlying routing engine). - Design quality: Clean. The recursive
Group(string, func(RouteRegister))pattern allows any package to define its own routes in isolation. TheInsertmethod enables adding routes to an existing group after the fact, which is used by plugins to inject plugin-specific routes into/api/plugins/.
Bus (event bus)#
- Package:
github.com/grafana/grafana/pkg/bus - File:
pkg/bus/bus.go:24 - Methods:
Publish(ctx context.Context, msg Msg) error AddEventListener(handler HandlerFunc) - Purpose: In-process publish/subscribe for domain events. Decouples producers from consumers when direct injection would create circular import cycles. Uses reflection to dispatch messages by type name.
- Implementations:
InProcBus(the only implementation). There is no distributed bus. - Design quality: Intentionally minimal, but the use of
HandlerFunc anyandMsg anysacrifices type safety — the dispatch is entirely reflection-based. Comment in the architecture docs acknowledges this is a legacy mechanism being phased out in favor of direct interface injection via Wire. The 2-method interface itself is well-segregated.
CacheStorage (distributed cache)#
- Package:
github.com/grafana/grafana/pkg/infra/remotecache - File:
pkg/infra/remotecache/remotecache.go:63 - Methods:
Get(ctx context.Context, key string) ([]byte, error) Set(ctx context.Context, key string, value []byte, expire time.Duration) error Delete(ctx context.Context, key string) error - Purpose: Swappable distributed cache abstraction. Implementations are selected at startup based on configuration. Provides a simple byte-slice cache so any serializable type can be cached across Grafana instances.
- Implementations:
MemcachedStorage,RedisStorage,DatabaseCache(using the SQL store as a fallback cache). - Design quality: Textbook ISP — 3 methods, fully orthogonal. The choice of
[]byterather thaninterface{}keeps marshalling in the caller and avoids the gob registration footgun at the interface boundary.
Tracer (observability)#
- Package:
github.com/grafana/grafana/pkg/infra/tracing - File:
pkg/infra/tracing/tracing.go:69 - Methods: (embeds
trace.Tracerfromgo.opentelemetry.io/otel/trace):Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) Inject(context.Context, http.Header, trace.Span) - Purpose: Grafana’s OpenTelemetry tracing surface. Embeds the standard OTel
trace.Tracerand addsInject()for HTTP header propagation (W3C TraceContext / B3). All infrastructure packages acceptTraceras a dependency. - Implementations:
TracingService(wraps OTel SDK). ANoopTraceris available for testing. - Design quality: The extension of stdlib/OTel interface is minimal and justified (one method added). Grafana avoids wrapping the full OTel SDK behind a custom interface — it only extends where necessary for HTTP propagation.
Interface patterns#
Size distribution: Highly varied. The most impactful interfaces are very small —
BackgroundService(1 method),Bus(2),CacheStorage(3),PluginSource(3),Tracer(~2). Domain service interfaces (DashboardServicewith 20 methods) are the outlier; they function as complete domain API contracts rather than narrow behavioral interfaces.Embedding: Used judiciously for protocol aggregation (
PluginClientembeds 8backend.*Handlerinterfaces from the SDK) and for OTel extension (Tracerembedstrace.Tracer). InfrastructureFSinterface embedsio/fs.FS. Not overused — most interfaces are defined flat.Implicit satisfaction: Grafana follows Go convention: interfaces are defined by consumers (in the same package as the consumer), not by providers.
DashboardServiceis defined inpkg/services/dashboards/, not in the implementation package.DBis defined inpkg/infra/db/, a thin package that the implementation (sqlstore.SQLStore) satisfies implicitly. This is intentional and enables test doubles without import cycles.stdlib interfaces used:
io/fs.FS— embedded inplugins.FScontext.Context— pervasive in all method signatureshttp.Handler— viaweb.Handlerwrapperhttp.Header— inTracer.Injecterror— idiomatic return type everywhere
Mock generation:
//go:generate mockerydirectives appear on service interfaces (DashboardService,Store,DashboardProvisioningService, etc.), generatingFake*structs in the same package. This is the project-wide standard for test doubles.
Key abstractions#
registry.BackgroundService— The most architecturally pivotal interface in the entire project. Its single-methodRun(ctx) errorcontract binds all 60+ background services into a uniform lifecycle. Nothing in the project better illustrates Grafana’s architectural philosophy: keep contracts minimal, use context for cancellation, let implementations vary freely.plugins.PluginClient— The gRPC boundary between Grafana and the plugin ecosystem. Everything external to the Grafana process is accessed through this interface. Its breadth (8 embedded handler interfaces) reflects the full plugin SDK protocol, not poor design.dashboards.DashboardService+dashboards.Store— The two-level service/store pattern replicated across all ~60 domain packages. Reading these two interfaces gives the mental model for the entire domain layer:Service= business logic contract,Store= persistence contract. Handlers depend onService;Serviceimplementations depend onStore.infra/db.DB— The infrastructure foundation. EveryStoreimplementation receives aDB. Its pragmatic inclusion ofGetEngine()andGetSqlxSession()documents the project’s migration path from xorm to sqlx without hiding the in-progress state behind a false abstraction.routing.RouteRegister— The HTTP extensibility spine. Its recursiveGroup()pattern is how 60+ service packages contribute routes to a single HTTP server without coupling to each other. TheInsert()method enables plugin route injection as a first-class feature.
Interface-driven extensibility#
Grafana uses interfaces for extensibility in three distinct ways:
Plugin isolation via
PluginClient: All data source, panel, and app plugins communicate through thePluginClientinterface over gRPC. Adding a new plugin type requires only implementing the relevantbackend.Handlerinterfaces from the SDK — the host never changes. This is the project’s primary extensibility mechanism.Route registration via
RouteRegister: Services, plugins, and the Kubernetes API integration all contribute routes viaRouteRegister. The plugin system callsrouter.Insert("/api/plugins/", ...)to inject plugin-specific endpoints. The new Resource API (/apis/...) is registered as a group. This keeps the HTTP surface decentralized while maintaining a single registered tree at startup.Backend swapping via infrastructure interfaces:
CacheStorage(Redis/Memcached/SQL),Tracer(OTel/NoopTracer),KVStore, andFileStorageare all interface-backed. Operators choose implementations via configuration; the application code never changes. This is the classic strategy pattern applied to infrastructure.Optional service behavior via
CanBeDisabled: Rather than a registration/deregistration mechanism, Grafana uses a type-assertion interface to allow services to opt out of starting. This avoids the need for a conditional registration API onBackgroundServiceRegistry.