Skip to content

Commit 7d22b41

Browse files
committed
feat: implement initial version of service
1 parent 63f883f commit 7d22b41

34 files changed

Lines changed: 1806 additions & 0 deletions

.dockerignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.git
2+
.gitignore
3+
bin
4+
tmp
5+
.DS_Store

.editorconfig

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[*]
2+
indent_style = space
3+
indent_size = 2
4+
end_of_line = lf
5+
insert_final_newline = true
6+
trim_trailing_whitespace = true
7+
charset = utf-8
8+
9+
[{Dockerfile,Dockerfile.*}]
10+
indent_size = 4
11+
tab_width = 4
12+
13+
[{Makefile,makefile,GNUmakefile}]
14+
indent_style = tab
15+
indent_size = 4
16+
17+
[Makefile.*]
18+
indent_style = tab
19+
indent_size = 4
20+
21+
[**/*.{go,mod,sum}]
22+
indent_style = tab
23+
indent_size = unset
24+
25+
[**/*.py]
26+
indent_size = 4

.github/dependabot.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "github-actions"
4+
directory: "/"
5+
schedule:
6+
interval: "weekly"
7+
8+
- package-ecosystem: "gomod"
9+
directory: "/"
10+
schedule:
11+
interval: "weekly"

.github/workflows/ci.yaml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
lint:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Checkout Code
19+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
20+
21+
- name: Setup Go
22+
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
23+
with:
24+
go-version: '1.26.2'
25+
26+
- name: Run go vet
27+
run: go vet ./...
28+
29+
- name: Check formatting
30+
run: |
31+
unformatted=$(gofmt -l .)
32+
if [ -n "$unformatted" ]; then
33+
echo "The following files are not formatted:"
34+
echo "$unformatted"
35+
exit 1
36+
fi
37+
38+
test-unit:
39+
runs-on: ubuntu-latest
40+
steps:
41+
- name: Checkout Code
42+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
43+
44+
- name: Setup Go
45+
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
46+
with:
47+
go-version: '1.26.2'
48+
49+
- name: Run unit tests
50+
run: make test-unit
51+
52+
test-integration:
53+
runs-on: ubuntu-latest
54+
steps:
55+
- name: Checkout Code
56+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
57+
58+
- name: Setup Go
59+
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
60+
with:
61+
go-version: '1.26.2'
62+
63+
- name: Run integration tests
64+
run: make test-integration
65+
66+
build:
67+
runs-on: ubuntu-latest
68+
steps:
69+
- name: Checkout Code
70+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
71+
72+
- name: Setup Go
73+
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
74+
with:
75+
go-version: '1.26.2'
76+
77+
- name: Build
78+
run: make build

.github/workflows/release.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: release
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
release:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout Code
16+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
17+
- name: Bump Version
18+
id: tag_version
19+
uses: mathieudutour/github-tag-action@a22cf08638b34d5badda920f9daf6e72c477b07b # v6.2.0
20+
with:
21+
github_token: ${{ secrets.GITHUB_TOKEN }}
22+
default_bump: minor
23+
custom_release_rules: bug:patch:Fixes,chore:patch:Chores,docs:patch:Documentation,feat:minor:Features,refactor:minor:Refactors,test:patch:Tests,ci:patch:Development,dev:patch:Development
24+
- name: Create Release
25+
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
26+
with:
27+
tag: ${{ steps.tag_version.outputs.new_tag }}
28+
name: ${{ steps.tag_version.outputs.new_tag }}
29+
body: ${{ steps.tag_version.outputs.changelog }}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: semantic-check
2+
on:
3+
pull_request:
4+
types:
5+
- opened
6+
- edited
7+
- synchronize
8+
9+
permissions:
10+
contents: read
11+
pull-requests: read
12+
13+
jobs:
14+
main:
15+
name: Semantic Commit Message Check
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Checkout Code
19+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
20+
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
21+
name: Check PR for Semantic Commit Message
22+
env:
23+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
24+
with:
25+
requireScope: false
26+
validateSingleCommit: true

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
!**/.gitkeep
2+
3+
tmp/
4+
dist/
5+
.DS_Store
6+
7+
.local/
8+
.env
9+
10+

Dockerfile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
FROM golang:1.26.2-alpine AS builder
2+
3+
WORKDIR /src
4+
5+
COPY go.mod go.sum ./
6+
RUN go mod download
7+
8+
COPY . .
9+
10+
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/server
11+
12+
FROM alpine:3.21
13+
14+
RUN adduser -D -g '' appuser
15+
16+
USER appuser
17+
WORKDIR /app
18+
19+
COPY --from=builder /out/server /app/server
20+
21+
EXPOSE 8080
22+
23+
ENTRYPOINT ["/app/server"]

Makefile

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
COMPOSE ?= docker compose
2+
3+
.PHONY: test test-unit test-integration build run docker-build up down logs demo-send demo health report
4+
5+
test:
6+
go test ./...
7+
8+
test-unit:
9+
go test ./...
10+
11+
test-integration:
12+
go test ./... -run Integration -count=1
13+
14+
build:
15+
go build ./...
16+
17+
run:
18+
go run ./cmd/server
19+
20+
docker-build:
21+
$(COMPOSE) build api
22+
23+
up:
24+
$(COMPOSE) up --build -d api
25+
26+
down:
27+
$(COMPOSE) down -v --remove-orphans
28+
29+
logs:
30+
$(COMPOSE) logs -f api
31+
32+
demo-send:
33+
$(COMPOSE) run --rm demo-sender
34+
35+
demo: up demo-send
36+
$(COMPOSE) logs api
37+
38+
health:
39+
@BASE_PATH="$${BASE_PATH:-/}"; \
40+
URL_PATH="$${BASE_PATH%/}/v1/manage/healthz"; \
41+
if [ -z "$${URL_PATH}" ]; then URL_PATH="/v1/manage/healthz"; fi; \
42+
curl -fsS "http://localhost:8080$${URL_PATH}"
43+
44+
report:
45+
@BASE_PATH="$${BASE_PATH:-/}"; \
46+
URL_PATH="$${BASE_PATH%/}/v1/reports"; \
47+
if [ -z "$${URL_PATH}" ]; then URL_PATH="/v1/reports"; fi; \
48+
curl -i -X POST \
49+
-H 'Content-Type: application/reports+json' \
50+
--data-binary @demo/reports.json \
51+
"http://localhost:8080$${URL_PATH}"

cmd/server/main.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
"net/http"
7+
"os"
8+
"os/signal"
9+
"syscall"
10+
"time"
11+
12+
"github.com/cruxstack/browser-reporting-api/internal/config"
13+
"github.com/cruxstack/browser-reporting-api/internal/httpx"
14+
"github.com/cruxstack/browser-reporting-api/internal/management"
15+
"github.com/cruxstack/browser-reporting-api/internal/parser"
16+
"github.com/cruxstack/browser-reporting-api/internal/reporting"
17+
"github.com/cruxstack/browser-reporting-api/internal/stream"
18+
)
19+
20+
func main() {
21+
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, nil)))
22+
23+
cfg, err := config.LoadFromEnv()
24+
if err != nil {
25+
slog.Error("load config", "error", err)
26+
os.Exit(1)
27+
}
28+
29+
writer := stream.NewNDJSONWriter(os.Stdout)
30+
reportsParser := parser.NewJSONBatchParser()
31+
reportsService := reporting.NewService(reportsParser, writer)
32+
originMatcher, err := httpx.NewOriginMatcher(cfg.AllowedOrigins)
33+
if err != nil {
34+
slog.Error("build origin matcher", "error", err)
35+
os.Exit(1)
36+
}
37+
38+
reportsHandler := reporting.NewHandler(reportsService, cfg.MaxBodyBytes, originMatcher)
39+
managementHandler := management.NewHandler()
40+
handler := httpx.NewRouter(cfg.BasePath, reportsHandler.Routes(), managementHandler.Routes())
41+
42+
srv := &http.Server{
43+
Addr: cfg.ListenAddr,
44+
Handler: handler,
45+
ReadHeaderTimeout: 5 * time.Second,
46+
ReadTimeout: 10 * time.Second,
47+
WriteTimeout: 10 * time.Second,
48+
IdleTimeout: 60 * time.Second,
49+
}
50+
51+
go func() {
52+
slog.Info("listening", "addr", cfg.ListenAddr, "base_path", cfg.BasePath)
53+
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
54+
slog.Error("server error", "error", err)
55+
os.Exit(1)
56+
}
57+
}()
58+
59+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
60+
defer stop()
61+
62+
<-ctx.Done()
63+
slog.Info("shutting down")
64+
65+
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
66+
defer cancel()
67+
68+
if err := srv.Shutdown(shutdownCtx); err != nil {
69+
slog.Error("graceful shutdown failed", "error", err)
70+
if closeErr := srv.Close(); closeErr != nil {
71+
slog.Error("force close failed", "error", closeErr)
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)