Skip to content

Conversation

@turip
Copy link
Member

@turip turip commented Dec 9, 2025

Overview

This allows us to use the same features/approach/tooling to the ingest endpoint as the other handlers.

Tests are passing, validated manually that ingest works.

Summary by CodeRabbit

Release Notes

  • New Features

    • Established formal ingest service with factory construction and configuration validation.
    • Added HTTP ingest endpoint with support for multiple content types: JSON, CloudEvents, and CloudEvents batch formats.
    • Implemented structured error handling for invalid content types and malformed events with appropriate HTTP 400 responses.
  • Refactor

    • Reorganized ingest HTTP transport layer for improved modularity and maintainability.

✏️ Tip: You can customize this high-level summary in your review settings.

This allows us to use the same features/approach/tooling to the ingest endpoint
as the other handlers.
@turip turip requested a review from a team as a code owner December 9, 2025 08:54
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 9, 2025

📝 Walkthrough

Walkthrough

This PR refactors the ingest service architecture by converting ingest.Service from a concrete struct to an interface, introducing a configuration-based factory pattern, and reorganizing HTTP transport logic into a new modular httpdriver package structure. The changes update dependency wiring across the application to use the new service and handler patterns.

Changes

Cohort / File(s) Summary
Service interface & factory
openmeter/ingest/service.go
Converts Service from a public struct to an interface, adds Config struct with validation, and introduces NewService(config) factory function. Internal receiver changes from public to private service type.
HTTP driver refactoring
openmeter/ingest/httpdriver/handler.go, openmeter/ingest/httpdriver/ingest.go, openmeter/ingest/httpdriver/errors.go
Introduces new httpdriver package with Handler interface, IngestEventsHandler, New() constructor, and error types. Replaces old ingestdriver/http_transport.go functionality.
Old HTTP transport removal
openmeter/ingest/ingestdriver/http_transport.go
Deleted file containing legacy NewIngestEventsHandler, error encoders, and request decoder logic now moved to httpdriver.
Service construction & wiring
app/common/openmeter_server.go
Updates NewIngestService to return ingest.Service (value) instead of *ingest.Service (pointer), uses ingest.NewService(config) factory.
Dependency injection updates
cmd/server/wire.go, cmd/server/wire_gen.go
Changes Application.IngestService field from *ingest.Service to ingest.Service (value type).
Server configuration & routing
cmd/server/main.go, openmeter/server/router/router.go, openmeter/server/router/event.go
Replaces IngestHandler with IngestService in config. Router now constructs ingesthttpdriver.Handler via New() and routes via IngestEvents().ServeHTTP().
Test updates
openmeter/ingest/httpdriver/ingest_test.go, openmeter/server/server_test.go
Migrates tests to new httpdriver package, updates service construction to use NewService(), creates NoopIngestService for testing.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Areas requiring extra attention:
    • Verification that all wiring points correctly use the new ingest.Service interface and NewService() factory
    • Ensure the value-type change to ingest.Service (from pointer) doesn't introduce unexpected nil-handling issues in downstream code
    • Confirm the new httpdriver package structure maintains all error handling paths from the deleted ingestdriver/http_transport.go
    • Validate that the Config.Validate() implementation properly catches misconfiguration early in all usage contexts

Suggested labels

kind/refactor, release-note/misc

Suggested reviewers

  • chrisgacsal
  • GAlexIHU

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main refactoring work: standardizing the ingest HTTP handler to align with other handlers' patterns and tooling.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/standardise-ingest-http-handler

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@turip turip added area/api release-note/misc Miscellaneous changes labels Dec 9, 2025
@turip turip enabled auto-merge (squash) December 9, 2025 08:58
@turip turip merged commit ee5be4e into main Dec 9, 2025
28 of 31 checks passed
@turip turip deleted the refactor/standardise-ingest-http-handler branch December 9, 2025 08:58
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
openmeter/ingest/httpdriver/errors.go (1)

11-49: Error types + encoder look good; just a couple of small nits

The overall shape (typed errors + errorEncoder delegating to commonhttp.HandleErrorIfTypeMatches) is clean and consistent.

A couple of tiny polish ideas you might consider:

  • In ErrorInvalidEvent.Error() / .Message(), calling e.Err.Error() will panic if Err is ever nil. Callers are currently always passing a non-nil error, but a quick guard (or formatting via fmt.Sprintf("invalid event: %v", e.Err)) would make this type safer against future misuse.
  • Error() and Message() currently duplicate the same string; if commonhttp doesn’t require them to differ, you could have one delegate to the other to keep them from drifting.
  • The commented-out old return lines can probably be dropped now to keep the file tidy.

None of these are blockers; the behavior as written is fine.

openmeter/ingest/httpdriver/handler.go (1)

22-47: Double‑check semantics for missing namespace → 500

resolveNamespace turns a missing namespace into:

return "", commonhttp.NewHTTPError(
    http.StatusInternalServerError,
    errors.New("internal server error"),
)

If GetNamespace can fail because of user input (e.g. bad path/host/tenant), that probably wants to be a 4xx (400/404/422) so clients get a “you mis‑addressed this” signal instead of a 500.

If, on the other hand, GetNamespace only returns ok == false when wiring/config is broken, then 500 is exactly right—just worth confirming that’s the case.

You might also consider reusing a shared/internal error value instead of duplicating the "internal server error" string here to keep things consistent with the rest of your error surface.

openmeter/ingest/httpdriver/ingest.go (1)

94-107: Bool return from Service.IngestEvents is currently unused

In the handler you do:

_, err := h.service.IngestEvents(ctx, params)
if err != nil {
    return IngestEventsResponse{}, err
}

Given Service.IngestEvents returns (bool, error) and the implementation in service.go always returns true when err == nil, that boolean isn’t adding anything at the moment.

Totally fine to ship as‑is, but worth aligning on intent:

  • If you don’t foresee partial‑success semantics, consider simplifying the interface to just error.
  • If you do plan to use the bool (e.g. “accepted vs. dropped”), you might eventually want to thread it into the HTTP response (status code or header) instead of ignoring it.

For now this is just a small design smell, not a blocker.

openmeter/ingest/service.go (2)

17-51: Config + constructor look good; consider reusing shared validation helper

The Service interface and Config + NewService pattern look clean and make wiring much nicer than constructing a concrete type directly.

Minor optional tweak: Config.Validate is hand‑rolling error aggregation with errors.Join, whereas the repo already has a generic validator helper in pkg/models/validator.go. If you want consistency across services, you could delegate to that helper instead of re‑implementing the pattern here.

Not a big deal either way; current code is totally fine.


58-96: Event processing is straightforward; a couple of small readability/design tweaks

The sequential loop over request.Events and the timestamp normalization + logging before collector.Ingest all look good and easy to reason about.

Two small things you might want to polish:

  • The parameter name in processEvent (event event.Event) shadows the imported event package, which can be a bit confusing when scanning the function. Renaming the param to ev (and, if desired, aliasing the import) would make it clearer.
  • As noted in the HTTP handler, IngestEvents’s bool return is always true on success and doesn’t currently drive any behavior. If you don’t expect partial-success semantics, simplifying to error would reduce a bit of noise; if you do, you might eventually want to expose that signal somewhere.

Functionally this all looks sound; these are just small cleanup ideas.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 18b337f and ea29722.

📒 Files selected for processing (13)
  • app/common/openmeter_server.go (1 hunks)
  • cmd/server/main.go (1 hunks)
  • cmd/server/wire.go (1 hunks)
  • cmd/server/wire_gen.go (1 hunks)
  • openmeter/ingest/httpdriver/errors.go (1 hunks)
  • openmeter/ingest/httpdriver/handler.go (1 hunks)
  • openmeter/ingest/httpdriver/ingest.go (1 hunks)
  • openmeter/ingest/httpdriver/ingest_test.go (6 hunks)
  • openmeter/ingest/ingestdriver/http_transport.go (0 hunks)
  • openmeter/ingest/service.go (3 hunks)
  • openmeter/server/router/event.go (1 hunks)
  • openmeter/server/router/router.go (5 hunks)
  • openmeter/server/server_test.go (2 hunks)
💤 Files with no reviewable changes (1)
  • openmeter/ingest/ingestdriver/http_transport.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

⚙️ CodeRabbit configuration file

**/*.go: In general when reviewing the Golang code make readability and maintainability a priority, even potentially suggest restructuring the code to improve them.

Performance should be a priority in critical code paths. Anything related to event ingestion, message processing, database operations (regardless of database) should be vetted for potential performance bottlenecks.

Files:

  • cmd/server/main.go
  • cmd/server/wire.go
  • openmeter/ingest/httpdriver/errors.go
  • app/common/openmeter_server.go
  • openmeter/server/server_test.go
  • openmeter/server/router/event.go
  • openmeter/server/router/router.go
  • openmeter/ingest/httpdriver/handler.go
  • openmeter/ingest/httpdriver/ingest.go
  • cmd/server/wire_gen.go
  • openmeter/ingest/service.go
  • openmeter/ingest/httpdriver/ingest_test.go
**/*_test.go

⚙️ CodeRabbit configuration file

**/*_test.go: Make sure the tests are comprehensive and cover the changes. Keep a strong focus on unit tests and in-code integration tests.
When appropriate, recommend e2e tests for critical changes.

Files:

  • openmeter/server/server_test.go
  • openmeter/ingest/httpdriver/ingest_test.go
🧠 Learnings (1)
📚 Learning: 2025-03-07T12:17:43.129Z
Learnt from: GAlexIHU
Repo: openmeterio/openmeter PR: 2383
File: openmeter/entitlement/metered/lateevents_test.go:37-45
Timestamp: 2025-03-07T12:17:43.129Z
Learning: In the OpenMeter codebase, test files like `openmeter/entitlement/metered/lateevents_test.go` may use variables like `meterSlug` and `namespace` without explicit declarations visible in the same file. This appears to be an accepted pattern in their test structure.

Applied to files:

  • openmeter/server/server_test.go
  • openmeter/server/router/router.go
🧬 Code graph analysis (10)
cmd/server/wire.go (1)
openmeter/ingest/service.go (1)
  • Service (13-15)
openmeter/ingest/httpdriver/errors.go (1)
pkg/framework/commonhttp/errors.go (1)
  • HandleErrorIfTypeMatches (63-76)
app/common/openmeter_server.go (1)
openmeter/ingest/service.go (3)
  • Service (13-15)
  • NewService (42-51)
  • Config (17-20)
openmeter/server/server_test.go (1)
openmeter/ingest/service.go (2)
  • Service (13-15)
  • IngestEventsRequest (53-56)
openmeter/server/router/router.go (1)
openmeter/ingest/service.go (1)
  • Service (13-15)
openmeter/ingest/httpdriver/handler.go (5)
openmeter/server/router/router.go (1)
  • IngestHandler (85-87)
openmeter/ingest/httpdriver/ingest.go (1)
  • IngestEventsHandler (20-20)
openmeter/ingest/service.go (1)
  • Service (13-15)
pkg/framework/transport/httptransport/options.go (1)
  • HandlerOption (19-21)
pkg/framework/commonhttp/errors.go (1)
  • NewHTTPError (36-42)
openmeter/ingest/httpdriver/ingest.go (6)
openmeter/ingest/service.go (1)
  • IngestEventsRequest (53-56)
openmeter/ingest/httpdriver/handler.go (2)
  • Handler (14-16)
  • New (37-47)
app/common/namespace.go (1)
  • Namespace (13-15)
api/api.gen.go (3)
  • IngestEventsBody (3835-3837)
  • IngestEventsApplicationCloudeventsPlusJSONRequestBody (9354-9354)
  • IngestEventsApplicationCloudeventsBatchPlusJSONBody (8503-8503)
openmeter/ingest/httpdriver/errors.go (2)
  • ErrorInvalidEvent (31-33)
  • ErrorInvalidContentType (11-13)
pkg/framework/commonhttp/encoder.go (1)
  • EmptyResponseEncoder (120-125)
cmd/server/wire_gen.go (1)
openmeter/ingest/service.go (1)
  • Service (13-15)
openmeter/ingest/service.go (3)
openmeter/ingest/httpdriver/ingest.go (1)
  • IngestEventsRequest (18-18)
openmeter/ingest/kafkaingest/collector.go (1)
  • Collector (30-39)
pkg/models/validator.go (1)
  • Validate (16-26)
openmeter/ingest/httpdriver/ingest_test.go (2)
openmeter/ingest/service.go (2)
  • NewService (42-51)
  • Config (17-20)
openmeter/ingest/httpdriver/handler.go (1)
  • New (37-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Lint
  • GitHub Check: Test
  • GitHub Check: Migration Checks
  • GitHub Check: Code Generators
  • GitHub Check: Build
  • GitHub Check: Repository Scan
  • GitHub Check: Analyze (go)
🔇 Additional comments (14)
openmeter/ingest/httpdriver/ingest_test.go (3)

23-70: LGTM! Clean migration to the service-based pattern.

The test correctly uses the new factory pattern with ingest.NewService and wires through httpdriver.New. Error handling is solid, and the test coverage is comprehensive.


72-95: Good error case coverage.

The test properly validates that invalid events return 400 Bad Request.


97-149: Excellent batch test coverage.

The batch handling test is thorough, creating 10 events and verifying they're all properly ingested.

openmeter/server/router/event.go (1)

10-12: Nice! Consistent with the handler pattern.

The change to a.ingestHandler.IngestEvents().ServeHTTP(w, r) aligns nicely with how other handlers in this file work (like a.meterEventHandler.ListEvents() on line 16).

cmd/server/main.go (1)

142-178: Clean separation of concerns.

Moving from IngestHandler to IngestService here is great—it pushes the HTTP transport concerns down to the router layer where they belong, keeping the main app wiring focused on business logic.

cmd/server/wire.go (1)

59-59: Interface value semantics—perfect.

Changing from *ingest.Service to ingest.Service is the right move since Service is an interface. This is more idiomatic and aligns with Go best practices.

cmd/server/wire_gen.go (1)

649-649: Consistent with wire.go.

The generated code correctly reflects the interface value type change.

openmeter/server/router/router.go (4)

104-104: Service-based configuration—nice and clean.

Adding IngestService ingest.Service to the config follows the same pattern as other services in this struct.


132-134: Validation in place.

The nil check for IngestService matches the validation pattern for other services in this function.


239-239: Handler field added to Router.

The ingestHandler ingesthttpdriver.Handler field follows the naming and type pattern of other handlers in this struct (like meterHandler, customerHandler, etc.).


301-305: Handler initialization consistent with the codebase pattern.

The initialization of ingestHandler using ingesthttpdriver.New with the namespace decoder, service, and error handler mirrors how other handlers are created (compare with meterHandler on lines 292-299).

openmeter/server/server_test.go (2)

1594-1604: Clean noop implementation for testing.

The NoopIngestService follows the same pattern as other noop services in this file (like NoopCustomerService, NoopBillingService, etc.). The type assertion on line 1596 ensures compile-time verification of interface compliance, which is a nice touch.


521-522: Test wiring updated correctly.

Replacing the handler with the service in the test config aligns with the refactored architecture.

app/common/openmeter_server.go (1)

107-115: Factory pattern with validation—excellent.

Switching to ingest.NewService is great because it centralizes service construction and validation logic. The return type change to ingest.Service (value) is correct for interfaces.

Comment on lines +35 to +90
contentType := r.Header.Get("Content-Type")

switch contentType {
case "application/json":
var apiRequest api.IngestEventsBody

err := json.NewDecoder(r.Body).Decode(&apiRequest)
if err != nil {
return req, ErrorInvalidEvent{
Err: err,
}
}

// Try to parse as a single event
e, err := apiRequest.AsEvent()
if err == nil {
req.Events = []event.Event{e}
} else {
// Try to parse as a batch of events
e, err := apiRequest.AsIngestEventsBody1()
if err == nil {
req.Events = e
}
}

// If we still don't have any events, return an error
if len(req.Events) == 0 {
return req, ErrorInvalidEvent{
Err: errors.New("no events found"),
}
}
case "application/cloudevents+json":
var apiRequest api.IngestEventsApplicationCloudeventsPlusJSONRequestBody

err := json.NewDecoder(r.Body).Decode(&apiRequest)
if err != nil {
return req, ErrorInvalidEvent{
Err: err,
}
}

req.Events = []event.Event{apiRequest}
case "application/cloudevents-batch+json":
var apiRequest api.IngestEventsApplicationCloudeventsBatchPlusJSONBody

err := json.NewDecoder(r.Body).Decode(&apiRequest)
if err != nil {
return req, ErrorInvalidEvent{
Err: err,
}
}

req.Events = apiRequest
default:
return req, ErrorInvalidContentType{ContentType: contentType}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "ingest.go" | grep httpdriver

Repository: openmeterio/openmeter

Length of output: 102


🏁 Script executed:

cat -n openmeter/ingest/httpdriver/ingest.go | sed -n '1,100p'

Repository: openmeterio/openmeter

Length of output: 3311


🏁 Script executed:

rg "IngestEvents" -A 5 --type go | grep -A 5 "func.*IngestEvents.*bool"

Repository: openmeterio/openmeter

Length of output: 881


🏁 Script executed:

fd -e go | xargs rg -l "IngestEvents" | head -5

Repository: openmeterio/openmeter

Length of output: 218


🏁 Script executed:

rg "type.*IngestEvents" --type go -B 2 -A 5 | head -30

Repository: openmeterio/openmeter

Length of output: 1511


🏁 Script executed:

rg "interface.*Ingest" --type go -B 2 -A 10

Repository: openmeterio/openmeter

Length of output: 47


🏁 Script executed:

python3 << 'EOF'
import subprocess
result = subprocess.run(['python3', '-c', 'import net.mime; print("net.mime available")'], 
                       capture_output=True, text=True)
# Just check if mime.ParseMediaType is in Go stdlib
print("Checking Go mime.ParseMediaType location...")
# In Go stdlib, mime.ParseMediaType is at net/mime package
print("Go stdlib: mime.ParseMediaType is in 'net/mime' package")
EOF

Repository: openmeterio/openmeter

Length of output: 151


🏁 Script executed:

rg "type.*Service.*interface" openmeter/ingest --type go -A 15

Repository: openmeterio/openmeter

Length of output: 829


Content‑Type handling is too strict and will reject real-world HTTP clients

The current code switches on the raw Content-Type header string:

contentType := r.Header.Get("Content-Type")

switch contentType {
case "application/json":

This will reject valid requests with application/json; charset=utf-8 (or similar with additional parameters), causing them to hit the default branch and fail with ErrorInvalidContentType. Since this is on the event ingestion hot path, this is a real correctness issue.

Use mime.ParseMediaType from the standard library to normalize:

import "net/mime"

mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
    return req, ErrorInvalidContentType{ContentType: r.Header.Get("Content-Type")}
}

switch mediaType {
case "application/json":

Additionally, improve error diagnostics in the JSON union parsing case (lines 49–65). When both AsEvent() and AsIngestEventsBody1() fail, the code currently returns a generic errors.New("no events found"), which discards the actual parsing error. Capturing and returning the last non-nil error from those helpers would significantly improve debuggability for clients and operators.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/api release-note/misc Miscellaneous changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants