Files
90CNServer/server/internal/platform/app/platform_wiring.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

598 lines
20 KiB
Go

package app
import (
"context"
"database/sql"
"fmt"
"log/slog"
"net/http"
"strings"
"sync/atomic"
"time"
"longheng.io/server/internal/platform/accountcenter"
"longheng.io/server/internal/platform/admincmdqueue"
"longheng.io/server/internal/platform/audit"
"longheng.io/server/internal/platform/bilog"
"longheng.io/server/internal/platform/bus"
cachekit "longheng.io/server/internal/platform/cache"
"longheng.io/server/internal/platform/config"
"longheng.io/server/internal/platform/datatable"
"longheng.io/server/internal/platform/eventlog"
"longheng.io/server/internal/platform/httpx"
lockkit "longheng.io/server/internal/platform/lock"
"longheng.io/server/internal/platform/logiclog"
"longheng.io/server/internal/platform/metrics"
"longheng.io/server/internal/platform/onlinepush"
"longheng.io/server/internal/platform/otelx"
"longheng.io/server/internal/platform/presence"
"longheng.io/server/internal/platform/ratelimit"
"longheng.io/server/internal/platform/registry"
"longheng.io/server/internal/platform/storageobject"
"longheng.io/server/internal/platform/topology"
_ "github.com/go-sql-driver/mysql"
)
func topologyConfig(cfg config.ServiceConfig) topology.Config {
return topology.Config{
Enabled: cfg.Topology.Enabled,
Services: cfg.Topology.Services,
GatewayService: cfg.Topology.GatewayService,
GatewayBackendService: cfg.Topology.GatewayBackendService,
RejectMaintaining: cfg.Topology.RejectMaintaining,
RefreshInterval: time.Duration(cfg.Topology.RefreshIntervalSeconds) * time.Second,
}
}
func newStorageObjSvc(cfg config.ServiceConfig, mux *http.ServeMux, wrapAdmin func(string, http.HandlerFunc) http.HandlerFunc, wrapDangerous func(string, string, http.HandlerFunc) http.HandlerFunc) (*storageobject.Service, error) {
if !cfg.StorageObject.Enabled {
return nil, nil
}
var store storageobject.Store
switch cfg.StorageObject.StoreKind {
case "", "memory":
store = storageobject.NewMemoryStore(nil)
case "mysql":
db, err := sql.Open("mysql", cfg.StorageObject.MySQLDSN)
if err != nil {
return nil, fmt.Errorf("open storage object mysql store: %w", err)
}
sqlStore, err := storageobject.NewSQLStore(storageobject.SQLOptions{
DB: db,
Table: cfg.StorageObject.MySQLTable,
})
if err != nil {
_ = db.Close()
return nil, fmt.Errorf("create storage object mysql store: %w", err)
}
if cfg.StorageObject.MySQLEnsureSchema {
if err := sqlStore.EnsureSchema(context.Background()); err != nil {
_ = sqlStore.Close()
return nil, fmt.Errorf("ensure storage object mysql schema: %w", err)
}
}
store = sqlStore
default:
return nil, fmt.Errorf("unsupported storage_object.store_kind %q", cfg.StorageObject.StoreKind)
}
service := storageobject.NewService(store)
if mux != nil {
wrap := func(next http.HandlerFunc) http.HandlerFunc {
if wrapAdmin != nil {
return wrapAdmin("storageobject", next)
}
return next
}
dangerous := func(operation string, next http.HandlerFunc) http.HandlerFunc {
if wrapDangerous != nil {
return wrapDangerous("storageobject", operation, next)
}
return wrap(next)
}
if err := storageobject.RegisterAdminRoutes(mux, service, storageobject.AdminOptions{Prefix: cfg.StorageObject.AdminPrefix, Wrap: wrap, WrapDangerous: dangerous}); err != nil {
_ = service.Close()
return nil, err
}
}
return service, nil
}
func newAccountCenter(cfg config.ServiceConfig, mux *http.ServeMux, wrapAdmin func(string, http.HandlerFunc) http.HandlerFunc, wrapDangerous func(string, string, http.HandlerFunc) http.HandlerFunc) (*accountcenter.Center, error) {
if !cfg.AccountCenter.Enabled {
return nil, nil
}
var store accountcenter.Store
switch cfg.AccountCenter.StoreKind {
case "", "memory":
case "mysql":
db, err := sql.Open("mysql", cfg.AccountCenter.MySQLDSN)
if err != nil {
return nil, fmt.Errorf("open account center mysql store: %w", err)
}
db.SetMaxOpenConns(16)
db.SetMaxIdleConns(4)
db.SetConnMaxLifetime(300 * time.Second)
sqlStore, err := accountcenter.NewSQLStore(accountcenter.SQLOptions{
DB: db,
Table: cfg.AccountCenter.MySQLTable,
})
if err != nil {
_ = db.Close()
return nil, fmt.Errorf("create account center mysql store: %w", err)
}
if cfg.AccountCenter.MySQLEnsureSchema {
if err := sqlStore.EnsureSchema(context.Background()); err != nil {
_ = sqlStore.Close()
return nil, fmt.Errorf("ensure account center mysql schema: %w", err)
}
}
store = sqlStore
default:
return nil, fmt.Errorf("unsupported account_center.store_kind %q", cfg.AccountCenter.StoreKind)
}
center, err := accountcenter.NewCenter(context.Background(), accountcenter.CenterOptions{Store: store})
if err != nil {
if closer, ok := store.(interface{ Close() error }); ok {
_ = closer.Close()
}
return nil, err
}
if mux != nil {
wrap := func(next http.HandlerFunc) http.HandlerFunc {
if wrapAdmin != nil {
return wrapAdmin("accountcenter", next)
}
return next
}
dangerous := func(operation string, next http.HandlerFunc) http.HandlerFunc {
if wrapDangerous != nil {
return wrapDangerous("accountcenter", operation, next)
}
return wrap(next)
}
if err := accountcenter.RegisterAdminRoutes(mux, center, accountcenter.AdminOptions{Prefix: cfg.AccountCenter.AdminPrefix, Wrap: wrap, WrapDangerous: dangerous}); err != nil {
_ = center.Close()
return nil, err
}
}
return center, nil
}
func newOnlinePushService(eventBus bus.Bus, presenceRuntime presence.Runtime, mux *http.ServeMux, wrapAdmin func(string, http.HandlerFunc) http.HandlerFunc, wrapDangerous func(string, string, http.HandlerFunc) http.HandlerFunc) (*onlinepush.Service, error) {
service := onlinepush.New(onlinepush.Options{Bus: eventBus, Presence: presenceRuntime})
if mux == nil {
return service, nil
}
wrap := func(next http.HandlerFunc) http.HandlerFunc {
if wrapAdmin != nil {
return wrapAdmin("onlinepush", next)
}
return next
}
dangerous := func(operation string, next http.HandlerFunc) http.HandlerFunc {
if wrapDangerous != nil {
return wrapDangerous("onlinepush", operation, next)
}
return wrap(next)
}
if err := onlinepush.RegisterAdminRoutes(mux, service, onlinepush.AdminOptions{Prefix: "/onlinepush", Wrap: wrap, WrapDangerous: dangerous}); err != nil {
return nil, err
}
return service, nil
}
func newRateLimiter(cfg config.ServiceConfig) (*ratelimit.Limiter, error) {
limitConfig, err := rateLimitConfig(cfg)
if err != nil {
return nil, err
}
return ratelimit.New(limitConfig), nil
}
func rateLimitConfig(cfg config.ServiceConfig) (ratelimit.Config, error) {
rules, err := ratelimit.ParseRules(cfg.RateLimit.Rules)
if err != nil {
return ratelimit.Config{}, fmt.Errorf("parse rate_limit.rules: %w", err)
}
return ratelimit.Config{
Enabled: cfg.RateLimit.Enabled,
Algorithm: cfg.RateLimit.Algorithm,
Window: time.Duration(cfg.RateLimit.WindowSeconds) * time.Second,
MaxRequests: cfg.RateLimit.MaxRequests,
Rules: rules,
CleanupEvery: time.Duration(cfg.RateLimit.CleanupIntervalSeconds) * time.Second,
TrustedHeader: cfg.RateLimit.TrustedHeader,
}, nil
}
func setupOTel(cfg config.ServiceConfig, logger *slog.Logger) (otelx.Shutdown, error) {
return otelx.Setup(context.Background(), cfg.OTel, cfg.Service.Name, cfg.Service.NodeID, cfg.Service.Environment, logger)
}
func newDataTables(cfg config.ServiceConfig) (*datatable.Registry, error) {
registry := datatable.NewRegistry(cfg.DataTables.Directory, cfg.DataTables.Version)
if !cfg.DataTables.Enabled {
return registry, nil
}
if err := registry.Load(context.Background()); err != nil {
return nil, fmt.Errorf("load data tables: %w", err)
}
return registry, nil
}
func newAuditLogger(cfg config.ServiceConfig) (*audit.Logger, error) {
if !cfg.Audit.Enabled {
return audit.NewLogger(cfg.Service.Name, cfg.Service.NodeID, nil), nil
}
var sink audit.Sink
switch cfg.Audit.Kind {
case "memory":
sink = audit.NewMemory(cfg.Audit.MemoryLimit)
case "file":
fileSink, err := audit.NewFile(cfg.Audit.FilePath, cfg.Audit.MemoryLimit)
if err != nil {
return nil, err
}
sink = fileSink
default:
return nil, fmt.Errorf("unsupported audit.kind %q", cfg.Audit.Kind)
}
return audit.NewLogger(cfg.Service.Name, cfg.Service.NodeID, sink), nil
}
func newLogicLogger(cfg config.ServiceConfig) (*logiclog.Logger, error) {
if !cfg.LogicLog.Enabled {
return logiclog.NewLogger(nil, nil), nil
}
catalog, err := logiclog.LoadCatalogCSV(cfg.LogicLog.ReasonsPath)
if err != nil {
return nil, fmt.Errorf("load logic_log.reasons_path: %w", err)
}
var sink logiclog.Sink
switch cfg.LogicLog.Kind {
case "memory":
sink = logiclog.NewBoundedMemorySink(cfg.LogicLog.MemoryLimit)
case "file":
fileSink, err := logiclog.NewFileSink(cfg.LogicLog.FilePath, cfg.LogicLog.MemoryLimit)
if err != nil {
return nil, err
}
sink = fileSink
default:
return nil, fmt.Errorf("unsupported logic_log.kind %q", cfg.LogicLog.Kind)
}
return logiclog.NewLogger(catalog, sink), nil
}
func newBILogger(cfg config.ServiceConfig) (*bilog.Logger, error) {
if !cfg.BILog.Enabled {
return bilog.NewLogger(nil, nil), nil
}
schema, err := bilog.LoadSchema(cfg.BILog.SchemaPath)
if err != nil {
return nil, fmt.Errorf("load bi_log.schema_path: %w", err)
}
var sink bilog.Sink
switch cfg.BILog.Kind {
case "memory":
sink = bilog.NewBoundedMemorySink(cfg.BILog.MemoryLimit)
case "file":
fileSink, err := bilog.NewFileSink(cfg.BILog.FilePath, cfg.BILog.MemoryLimit)
if err != nil {
return nil, err
}
sink = fileSink
default:
return nil, fmt.Errorf("unsupported bi_log.kind %q", cfg.BILog.Kind)
}
return bilog.NewLogger(schema, sink), nil
}
func newEventLog(cfg config.ServiceConfig, eventBus bus.Bus, mux *http.ServeMux, wrapAdmin func(string, http.HandlerFunc) http.HandlerFunc, logger *slog.Logger) (*eventlog.Log, Component, error) {
if !cfg.EventLog.Enabled {
return nil, nil, nil
}
var store eventlog.Store
switch cfg.EventLog.StoreKind {
case "", "memory":
store = eventlog.NewMemoryStore(cfg.Service.Name + "-outbox")
case "mysql":
db, err := sql.Open("mysql", cfg.EventLog.MySQLDSN)
if err != nil {
return nil, nil, fmt.Errorf("open eventlog mysql store: %w", err)
}
db.SetMaxOpenConns(cfg.EventLog.MySQLMaxOpenConns)
db.SetMaxIdleConns(cfg.EventLog.MySQLMaxIdleConns)
db.SetConnMaxLifetime(time.Duration(cfg.EventLog.MySQLConnMaxLifetime) * time.Second)
sqlStore, err := eventlog.NewSQLStoreFromDB(db, eventlog.SQLStoreOptions{
Name: cfg.Service.Name + "-outbox",
Table: cfg.EventLog.MySQLTable,
})
if err != nil {
_ = db.Close()
return nil, nil, fmt.Errorf("create eventlog mysql store: %w", err)
}
if cfg.EventLog.MySQLEnsureSchema {
if err := sqlStore.EnsureSchema(context.Background()); err != nil {
_ = sqlStore.Close()
return nil, nil, fmt.Errorf("ensure eventlog mysql schema: %w", err)
}
}
store = sqlStore
default:
return nil, nil, fmt.Errorf("unsupported eventlog.store_kind %q", cfg.EventLog.StoreKind)
}
log := eventlog.New(eventlog.Options{
Name: cfg.Service.Name + "-outbox",
Store: store,
IDGenerator: newEventIDGen(cfg),
})
if mux != nil {
wrap := func(next http.HandlerFunc) http.HandlerFunc {
if wrapAdmin != nil {
return wrapAdmin("eventlog", next)
}
return httpx.WrapAdmin(cfg.Admin.Token, logger, "eventlog", next)
}
if err := eventlog.RegisterAdminRoutes(mux, log, eventlog.AdminOptions{Prefix: cfg.EventLog.AdminPrefix, Wrap: wrap}); err != nil {
_ = log.Close()
return nil, nil, err
}
}
if !cfg.EventLog.PublishEnabled {
return log, nil, nil
}
publisher, err := newEventLogPublisher(cfg, eventBus)
if err != nil {
_ = log.Close()
return nil, nil, err
}
worker, err := eventlog.NewPublishWorker(eventlog.PublishWorkerOptions{
Name: cfg.Service.Name + "-eventlog-publisher",
Log: log,
Publisher: publisher,
Interval: time.Duration(cfg.EventLog.PublishIntervalSeconds) * time.Second,
Limit: cfg.EventLog.PublishBatchSize,
RetryDelay: time.Duration(cfg.EventLog.PublishRetrySeconds) * time.Second,
MaxAttempts: cfg.EventLog.PublishMaxAttempts,
ExcludeStreams: []string{admincmdqueue.DefaultStream},
Logger: logger,
})
if err != nil {
_ = log.Close()
return nil, nil, err
}
return log, worker, nil
}
func newEventLogPublisher(cfg config.ServiceConfig, eventBus bus.Bus) (eventlog.Publisher, error) {
switch cfg.EventLog.PublishKind {
case "", "bus":
return newEventBusPub(eventBus)
case "http":
return newEventLogPub(cfg)
case "bus_http":
busPublisher, err := newEventBusPub(eventBus)
if err != nil {
return nil, err
}
httpPublisher, err := newEventLogPub(cfg)
if err != nil {
return nil, err
}
return eventlog.NewFanoutPublisher(busPublisher, httpPublisher)
default:
return nil, fmt.Errorf("unsupported eventlog.publish_kind %q", cfg.EventLog.PublishKind)
}
}
func newEventBusPub(eventBus bus.Bus) (*eventlog.BusPublisher, error) {
return eventlog.NewBusPublisher(eventBus, eventlog.BusPublisherOptions{
Topic: eventlog.OriginalTopic,
Payload: eventlog.OriginalTopicPayload,
})
}
func newEventLogPub(cfg config.ServiceConfig) (*eventlog.HTTPPublisher, error) {
body := eventlog.EventJSONBody
if cfg.EventLog.PublishHTTPPayload == "payload" {
body = eventlog.PayloadJSONBody
}
return eventlog.NewHTTPPublisher(eventlog.HTTPPublisherOptions{
URL: cfg.EventLog.PublishHTTPURL,
Method: cfg.EventLog.PublishHTTPMethod,
Client: &http.Client{Timeout: time.Duration(cfg.EventLog.PublishHTTPTimeout) * time.Second},
Headers: cfg.EventLog.PublishHTTPHeaders,
Body: body,
})
}
func newEventIDGen(cfg config.ServiceConfig) eventlog.IDGenerator {
prefix := eventLogIDPrefix(cfg)
var seq uint64
return func() string {
n := atomic.AddUint64(&seq, 1)
return fmt.Sprintf("%s-%d-%d", prefix, time.Now().UTC().UnixNano(), n)
}
}
func eventLogIDPrefix(cfg config.ServiceConfig) string {
service := cleanEventIDPart(cfg.Service.Name)
node := cleanEventIDPart(cfg.Service.NodeID)
if service == "" {
service = "service"
}
if node == "" {
node = "node"
}
prefix := service + "-" + node
if len(prefix) > 96 {
return prefix[:96]
}
return prefix
}
func cleanEventIDPart(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
var b strings.Builder
lastDash := false
for _, r := range value {
ok := (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_'
if ok {
b.WriteRune(r)
lastDash = false
continue
}
if !lastDash {
b.WriteByte('-')
lastDash = true
}
}
return strings.Trim(b.String(), "-")
}
func newRegistry(cfg config.ServiceConfig, logger *slog.Logger) (registry.Registry, error) {
switch cfg.Registry.Kind {
case "", "memory":
return registry.NewMemory(), nil
case "redis":
return registry.NewRedis(cfg.Registry.Endpoints, cfg.Registry.Namespace, cfg.Registry.LeaseTTL, cfg.Registry.RedisPassword, cfg.Registry.RedisDB, logger)
case "etcd":
return registry.NewETCD(cfg.Registry.Endpoints, cfg.Registry.Namespace, cfg.Registry.LeaseTTL, logger)
default:
return nil, fmt.Errorf("unsupported registry.kind %q", cfg.Registry.Kind)
}
}
func newBus(cfg config.ServiceConfig, logger *slog.Logger) (bus.Bus, error) {
switch cfg.Bus.Kind {
case "", "memory":
return bus.NewMemory(logger), nil
case "redis":
return bus.NewRedisWithOptions(cfg.Bus.Endpoints, cfg.Bus.Namespace, logger, bus.RedisOptions{
Username: cfg.Bus.RedisUsername,
Password: cfg.Bus.RedisPassword,
Database: cfg.Bus.RedisDB,
TLSEnabled: cfg.Bus.RedisTLSEnabled,
TLSServerName: cfg.Bus.RedisTLSServerName,
})
case "nats":
return bus.NewNATSWithOptions(cfg.Bus.Endpoints, cfg.Bus.Namespace, logger, bus.NATSOptions{
Name: cfg.Bus.NATSName,
Timeout: time.Duration(cfg.Bus.NATSTimeoutSeconds) * time.Second,
PingInterval: time.Duration(cfg.Bus.NATSPingIntervalSeconds) * time.Second,
MaxPingsOutstanding: cfg.Bus.NATSMaxPingsOutstanding,
MaxReconnects: cfg.Bus.NATSMaxReconnects,
ReconnectWait: time.Duration(cfg.Bus.NATSReconnectWaitSeconds) * time.Second,
RetryOnFailedConnect: true,
WireEncoding: cfg.Bus.NATSWireEncoding,
Token: cfg.Bus.NATSToken,
Username: cfg.Bus.NATSUsername,
Password: cfg.Bus.NATSPassword,
Credentials: cfg.Bus.NATSCredentials,
TLSEnabled: cfg.Bus.NATSTLSEnabled,
TLSServerName: cfg.Bus.NATSTLSServerName,
CAFile: cfg.Bus.NATSCAFile,
CertFile: cfg.Bus.NATSCertFile,
KeyFile: cfg.Bus.NATSKeyFile,
})
default:
return nil, fmt.Errorf("unsupported bus.kind %q", cfg.Bus.Kind)
}
}
// busMetricsAttacher 由所有 bus adapter 实现,让平台 wiring 可以挂接 Metrics 发射器,
// 而不需要绑定具体类型。
type busMetricsAttacher interface {
SetMetrics(*bus.Metrics)
}
func attachBusMetrics(eventBus bus.Bus, reg *metrics.Registry, cfg config.ServiceConfig) {
if reg == nil || eventBus == nil {
return
}
kind := cfg.Bus.Kind
if kind == "" {
kind = "memory"
}
attacher, ok := eventBus.(busMetricsAttacher)
if !ok {
return
}
attacher.SetMetrics(bus.NewMetrics(reg, kind))
}
func newCache(cfg config.ServiceConfig, reg *metrics.Registry) (cachekit.Store, error) {
ttl := time.Duration(cfg.Cache.DefaultTTLSeconds) * time.Second
kind := cfg.Cache.Kind
if kind == "" {
kind = "memory"
}
var store cachekit.Store
switch cfg.Cache.Kind {
case "", "memory":
store = cachekit.NewMemory(cachekit.MemoryOptions{
Name: cfg.Service.Name + "-cache",
DefaultTTL: ttl,
MaxEntries: cfg.Cache.MaxEntries,
})
case "redis":
store = cachekit.NewRedis(cachekit.RedisOptions{
Name: cfg.Service.Name + "-cache",
KeyPrefix: cfg.Cache.KeyPrefix,
Address: cfg.Cache.RedisAddress,
Password: cfg.Cache.RedisPassword,
DB: cfg.Cache.RedisDB,
PoolSize: cfg.Cache.RedisPoolSize,
Timeout: time.Duration(cfg.Cache.RedisTimeoutSeconds) * time.Second,
ConnectTimeout: time.Duration(cfg.Cache.RedisConnectTimeout) * time.Second,
ReadTimeout: time.Duration(cfg.Cache.RedisReadTimeout) * time.Second,
WriteTimeout: time.Duration(cfg.Cache.RedisWriteTimeout) * time.Second,
DefaultTTL: ttl,
})
default:
return nil, fmt.Errorf("unsupported cache.kind %q", cfg.Cache.Kind)
}
if attacher, ok := store.(interface{ SetMetrics(*cachekit.Metrics) }); ok {
attacher.SetMetrics(cachekit.NewMetrics(reg, kind))
}
return store, nil
}
func newLockManager(cfg config.ServiceConfig, reg *metrics.Registry) (lockkit.Manager, error) {
ttl := time.Duration(cfg.Lock.TTLSeconds) * time.Second
kind := cfg.Lock.Kind
if kind == "" {
kind = "memory"
}
var manager lockkit.Manager
switch cfg.Lock.Kind {
case "", "memory":
manager = lockkit.NewMemory(lockkit.MemoryOptions{Name: cfg.Service.Name + "-lock", DefaultTTL: ttl})
case "redis":
manager = lockkit.NewRedis(lockkit.RedisOptions{
Name: cfg.Service.Name + "-lock",
KeyPrefix: cfg.Lock.KeyPrefix,
Address: cfg.Lock.RedisAddress,
Password: cfg.Lock.RedisPassword,
DB: cfg.Lock.RedisDB,
PoolSize: cfg.Lock.RedisPoolSize,
Timeout: time.Duration(cfg.Lock.RedisTimeoutSeconds) * time.Second,
ConnectTimeout: time.Duration(cfg.Lock.RedisConnectTimeout) * time.Second,
ReadTimeout: time.Duration(cfg.Lock.RedisReadTimeout) * time.Second,
WriteTimeout: time.Duration(cfg.Lock.RedisWriteTimeout) * time.Second,
DefaultTTL: ttl,
})
default:
return nil, fmt.Errorf("unsupported lock.kind %q", cfg.Lock.Kind)
}
if attacher, ok := manager.(interface{ SetMetrics(*lockkit.Metrics) }); ok {
attacher.SetMetrics(lockkit.NewMetrics(reg, kind))
}
return manager, nil
}