Refactor: Centralize Observability in MongoDB & ArangoDB Modules#3102
Refactor: Centralize Observability in MongoDB & ArangoDB Modules#3102akshat-kumar-singhal wants to merge 30 commits intogofr-dev:developmentfrom
Conversation
…arameters and improve readability
Updated tests for mongo tp include tests for validating call to instrumenter
Moved mocks to separate folder
…and call that from arango/mongo to reduce duplication. Moved to use ctx.Background() in the RecordHistogram, in line with original implementation
There was a problem hiding this comment.
Hey, thanks for putting this together — I can see the intent behind centralizing observability and it's clear a lot of thought went into it.
However, I'd like you to reconsider the approach here. I have a few concerns:
Cross-module coupling
Previously, each datasource (mongo, arangodb, etc.) defined its own Logger and Metrics interfaces locally via duck typing — fully independent, zero shared dependencies. This PR introduces a shared observability module that all datasource modules must now import, which means:
- If
observabilityinterfaces change (e.g., adding a method toLogger,Metrics, orObservableQuery), every dependent datasource module must be updated and released in lockstep. - Third-party datasource authors now take on a GoFr-internal dependency they didn't need before.
The ObservableQuery interface is too opinionated
It forces every datasource's QueryLog to conform to SetDuration, GetOperation, GetMetricLabels, GetTraceLabels. Different datasources have different observability needs — this abstraction becomes a lowest common denominator.
The defer bug fix doesn't require this scope
The original bug (defer placed after error check, span leak, wrong timing) is valid and worth fixing, but the fix is ~2-3 lines per method — move defer to the top, capture time.Now() before the operation. A simple helper in each datasource achieves the same structural safety:
func startOperation(ctx context.Context, tracer trace.Tracer, spanName string,
attrs map[string]string) (context.Context, trace.Span, time.Time) {
startTime := time.Now()
if tracer == nil {
return ctx, nil, startTime
}
ctx, span := tracer.Start(ctx, spanName)
for k, v := range attrs {
span.SetAttributes(attribute.String(k, v))
}
return ctx, span, startTime
}
func endOperation(span trace.Span, startTime time.Time, durationKey string,
logger Logger, ql *QueryLog, metrics Metrics, histogram string, labels ...string) {
duration := time.Since(startTime).Microseconds()
ql.Duration = duration
logger.Debug(ql)
metrics.RecordHistogram(context.Background(), histogram, float64(duration)/1e6, labels...)
if span != nil {
span.SetAttributes(attribute.Int64(durationKey, duration))
span.End()
}
}Yes, this is duplicated across mongo, arangodb, etc. But:
- It's ~20 lines of straightforward code
- Each datasource can tweak it freely
- Zero cross-module dependencies
- Zero interfaces
- Zero new Go modules
- The defer bug is still fixed
What I think is worth keeping
The instrumentDatasource() consolidation in external_db.go is a genuine improvement — the old code had 15+ identical UseLogger/UseMetrics/UseTracer/Connect blocks. But that can be a private helper in the gofr package using anonymous type assertions, no separate module needed:
func (a *App) instrumentDatasource(ds any, tracerName string) {
if l, ok := ds.(interface{ UseLogger(any) }); ok {
l.UseLogger(a.Logger())
}
if m, ok := ds.(interface{ UseMetrics(any) }); ok {
m.UseMetrics(a.Metrics())
}
if t, ok := ds.(interface{ UseTracer(any) }); ok {
t.UseTracer(otel.GetTracerProvider().Tracer(tracerName))
}
if c, ok := ds.(interface{ Connect() }); ok {
c.Connect()
}
}Summary
A whole module + interfaces + noop implementations + mocks for what amounts to "start timer, start span, defer end" feels like over-engineering. I'd suggest:
- Fix the defer bug directly in each affected datasource (small, targeted PR).
- Consolidate
external_db.gowiring with a private helper (separate PR). - Keep datasource observability local — each module owns its own logging, metrics, and tracing decisions.
Happy to discuss further if you see it differently!
We could split the
Breaking change in the interface won't be done unless it's a major release.
The concern is understandable - it however should get addressed by splitting the Instrumenter interface - data services interested to use the common capability from the observability package would continue to do it.
Keeping it local would remove the standardisation - this goes against the idea of an opinionated framework. |
Description:
pkg/gofr/datasource/observabilitypackageAddTraceandOperationStatsmethods to reduce function parameters and improve readabilitylogger,metrics,tracerfields with a singleinstrumentationfield in both ArangoDB and MongoDB datasourcesSetLogger(),SetMetrics(),SetTracer()methods while keeping deprecatedUseLogger(),UseMetrics(),UseTracer()for backward compatibilityQueryLogstruct withSetDuration(),GetOperation(), andMeta()helper methodscontainer/datasources.goas deprecated in favor ofdatasource.ObservablepatternBreaking Changes (if applicable):
There are no breaking changes in this, however some deprecations/ cleanups/ standardisations:
MongoProvider,ArangoDBProvider, etc.) are now deprecated; users should migrate todatasource.ObservableaddTrace→instrumentation.AddTrace,sendOperationStats→instrumentation.OperationStats)metrics.go,mock_logger.go, andmock_metrics.gofiles fromarangodbandmongopackages (consolidated into observability package)hostnametoapp_arango_statsin addition toendpointwith the same value for backward compatibility purposes.endpointlabel to be deprecated. Note: since bothhostnameandendpointhave the same value, it would not cause an increase in the cardinality.Additional Information:
pkg/gofr/datasource/observabilitywith its owngo.modandgo.sumChecklist:
goimportandgolangci-lint.