Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1d238fd
feat: add docker-compose local dev stack with profiles
ankurs Apr 8, 2026
97f763e
fix: remove spurious 'build-' prefix from Dockerfile app label
ankurs Apr 8, 2026
3f37915
feat: add Grafana dashboard, include_docker_compose flag, deploy/loca…
ankurs Apr 8, 2026
c686cab
feat: infra-only local-stack, load testing, dynamic endpoint display
ankurs Apr 8, 2026
3dbf8e6
fix: stop gitignoring misc/, add loadtest config
ankurs Apr 8, 2026
c0c6e0b
fix: add --remove-orphans to local-stack, fix gitignore test
ankurs Apr 8, 2026
a8110cc
feat: add metrics package with interface, promauto, and sample usage
ankurs Apr 8, 2026
b3eb038
feat: add Jaeger to obs profile for OTEL distributed tracing
ankurs Apr 8, 2026
5f90a49
fix: address PR review — portable compose, rm cleanup, psql exec, YAM…
ankurs Apr 8, 2026
a134364
feat: per-service docker-compose profiles with inline option selection
ankurs Apr 8, 2026
2829222
fix: AlloyDB, local-exec, obs shortcut, code review fixes
ankurs Apr 8, 2026
ed0c4d1
fix: stale deps reference in AGENTS.md load testing note
ankurs Apr 8, 2026
8722574
fix: PR review — Grafana UID, health checks, Linux support, skip warning
ankurs Apr 8, 2026
31c1d91
fix: ministack image name, gatherMetric error handling, alloydb in Ma…
ankurs Apr 8, 2026
58d690a
fix: local-stack-obs reuses endpoint display, local-stack-down uses -…
ankurs Apr 8, 2026
f05db4e
chore: reorder cookiecutter prompts, update README features list
ankurs Apr 8, 2026
d1c867c
fix: sanitize hyphens/dots in Prometheus metrics namespace
ankurs Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
"docker_image": "alpine:latest",
"docker_build_image": "golang",
"docker_build_image_version": ["1.26", "1.25"],
"include_docker_compose": true,
"_copy_without_render": [
"third_party/OpenAPI/swagger-ui*",
"third_party/*js.map",
"third_party/*css.map",
"third_party/*css"
"third_party/*css",
"deploy/local/grafana/*"
]
}
18 changes: 17 additions & 1 deletion hooks/post_gen_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,22 @@ def setup_local_env():
if os.path.exists(example) and not os.path.exists(local):
shutil.copy2(example, local)

init_proto()
def remove_docker_compose():
"""
Removes docker-compose and deploy files when include_docker_compose is false
"""
import shutil
remove_file("docker-compose.local.yml")
deploy_dir = os.path.join(PROJECT_DIRECTORY, "deploy")
if os.path.exists(deploy_dir):
shutil.rmtree(deploy_dir)

Comment thread
ankurs marked this conversation as resolved.
if os.environ.get("COOKIECUTTER_SKIP_PROTO_INIT") != "1":
init_proto()

setup_local_env()

if "{{ cookiecutter.include_docker_compose }}".lower() not in ("true", "1", "yes"):
remove_docker_compose()
Comment thread
ankurs marked this conversation as resolved.

init_git()
34 changes: 25 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,42 @@ def default_context():

@pytest.fixture
def bake_project(tmp_path, template_dir, default_context):
"""Factory fixture that bakes a project without running hooks.
"""Factory fixture that bakes a project.

Returns a function that accepts optional context overrides and
returns a pathlib.Path to the generated project directory.

Pass with_hooks=True to enable post-generation hooks (skips proto init
via COOKIECUTTER_SKIP_PROTO_INIT=1 to keep tests fast).
"""
def _bake(extra_context=None, full_context=None):
def _bake(extra_context=None, full_context=None, with_hooks=False):
if full_context is not None:
ctx = full_context
else:
ctx = {**default_context}
if extra_context:
ctx.update(extra_context)

project_dir = cookiecutter(
template_dir,
output_dir=str(tmp_path),
no_input=True,
extra_context=ctx,
accept_hooks=False,
)
import os
old_env = os.environ.get("COOKIECUTTER_SKIP_PROTO_INIT")
if with_hooks:
os.environ["COOKIECUTTER_SKIP_PROTO_INIT"] = "1"

try:
project_dir = cookiecutter(
template_dir,
output_dir=str(tmp_path),
no_input=True,
extra_context=ctx,
accept_hooks=with_hooks,
)
finally:
if with_hooks:
if old_env is None:
os.environ.pop("COOKIECUTTER_SKIP_PROTO_INIT", None)
else:
os.environ["COOKIECUTTER_SKIP_PROTO_INIT"] = old_env

return Path(project_dir)

return _bake
70 changes: 69 additions & 1 deletion tests/test_cookiecutter_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ def test_expected_files_exist(self, bake_project):
"main.go",
"Makefile",
"Dockerfile",
"docker-compose.local.yml",
"deploy/local/prometheus.yml",
"deploy/local/grafana/provisioning/datasources/prometheus.yml",
"deploy/local/grafana/provisioning/dashboards/dashboards.yml",
"deploy/local/grafana/dashboards/coldbrew-service.json",
"misc/loadtest/echo.json",
"go.mod",
"README.md",
"AGENTS.md",
Expand Down Expand Up @@ -277,6 +283,16 @@ def test_go_tool_commands(self, bake_project):
assert "go tool mockery" in content
assert "go tool govulncheck" in content

def test_local_stack_targets(self, bake_project):
project = bake_project()
content = (project / "Makefile").read_text()
assert "local-stack:" in content
assert "local-stack-down:" in content
assert "local-stack-logs:" in content
assert "local-stack-reset:" in content
assert "local-psql:" in content
assert "docker-compose.local.yml" in content

def test_bench_run_pattern(self, bake_project):
project = bake_project()
content = (project / "Makefile").read_text()
Expand Down Expand Up @@ -346,11 +362,63 @@ def test_gitlab_ci_go_tool_cobertura(self, bake_project):
# ---------------------------------------------------------------------------


class TestDockerCompose:
def test_compose_infra_services(self, bake_project):
project = bake_project()
content = (project / "docker-compose.local.yml").read_text()
assert "db:" in content
assert "redis:" in content
assert "prometheus:" in content
assert "grafana:" in content

def test_compose_profiles(self, bake_project):
project = bake_project()
content = (project / "docker-compose.local.yml").read_text()
assert 'profiles: ["deps"]' in content
assert 'profiles: ["obs"]' in content

def test_compose_db_name(self, bake_project):
project = bake_project()
content = (project / "docker-compose.local.yml").read_text()
assert "POSTGRES_DB: testservice_dev" in content

def test_agents_md_local_stack(self, bake_project):
project = bake_project()
content = (project / "AGENTS.md").read_text()
assert "make local-stack" in content
assert "PROFILES=" in content
assert "localhost:9091" in content

def test_grafana_provisioning_exists(self, bake_project):
project = bake_project()
assert (project / "deploy/local/grafana/provisioning/datasources/prometheus.yml").exists()
assert (project / "deploy/local/grafana/provisioning/dashboards/dashboards.yml").exists()
assert (project / "deploy/local/grafana/dashboards/coldbrew-service.json").exists()

def test_grafana_volumes_in_compose(self, bake_project):
project = bake_project()
content = (project / "docker-compose.local.yml").read_text()
assert "deploy/local/grafana/provisioning:/etc/grafana/provisioning" in content
assert "deploy/local/grafana/dashboards:/var/lib/grafana/dashboards" in content

def test_loadtest_config(self, bake_project):
project = bake_project()
content = (project / "misc/loadtest/echo.json").read_text()
assert "com.github.testorg.TestSvc/Echo" in content
assert '"reflect": true' in content
assert "localhost:9090" in content

def test_docker_compose_disabled(self, bake_project):
project = bake_project({"include_docker_compose": "false"}, with_hooks=True)
assert not (project / "docker-compose.local.yml").exists()
assert not (project / "deploy").exists()


class TestConfigFiles:
def test_gitignore_entries(self, bake_project):
project = bake_project()
content = (project / ".gitignore").read_text()
for entry in ["local.env", "cover.html", "cover.out", "misc"]:
for entry in ["local.env", "cover.html", "cover.out"]:
assert entry in content

def test_local_env_example_exists(self, bake_project):
Expand Down
1 change: 0 additions & 1 deletion {{cookiecutter.app_name}}/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@ vendor
cover.out
cover.html
local.env
misc
41 changes: 41 additions & 0 deletions {{cookiecutter.app_name}}/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,47 @@ GOPRIVATE is pre-configured in Makefile, Dockerfile, and CI workflows. For priva
- **CI**: uncomment the auth steps in `.github/workflows/go.yml` or `.gitlab-ci.yml`
- See [Private Modules guide](https://docs.coldbrew.cloud/howto/private-modules/) for details

## Local Development Stack

Start infrastructure with docker-compose, then run the app locally with `make run`:

```bash
# Start infrastructure
make local-stack PROFILES="deps" # Postgres, Redis, Adminer
make local-stack PROFILES="deps obs" # + Prometheus, Grafana (with pre-built dashboard)

# Run the app (fast native build, no Docker)
make run

# Infrastructure management
make local-stack-logs # follow infra logs
make local-stack-down PROFILES="deps" # stop infra
make local-stack-reset PROFILES="deps" # stop, remove, restart
make local-psql # open Postgres shell
```

Endpoints:
- Service HTTP/Swagger: http://localhost:9091/swagger/ (via `make run`)
- Service gRPC: localhost:9090
- Postgres: localhost:5433 (user: postgres, password: postgres, db: {{cookiecutter.app_name}}_dev)
- Redis: localhost:6379
- Adminer (DB UI): http://localhost:8088
- Prometheus: http://localhost:9100
- Grafana: http://localhost:3000 (admin/admin) — ColdBrew dashboard pre-loaded

## Load Testing

Run gRPC load tests against a locally running service using [ghz](https://ghz.sh):

```bash
make run # start the app in one terminal
make loadtest # run load test in another terminal
```

The default config (`misc/loadtest/echo.json`) sends 1000 requests at concurrency 10 to the Echo RPC via gRPC reflection. Edit the file to adjust total, concurrency, or target a different RPC.

With the obs profile running (`make local-stack PROFILES="deps obs"`), load test results are visible in the Grafana dashboard in real-time.

## Rules

- **Never edit generated files** — files in `proto/*.pb.go`, `proto/*_grpc.pb.go`, `proto/*.gw.go` are generated. Edit the `.proto` file and run `make generate`.
Expand Down
2 changes: 1 addition & 1 deletion {{cookiecutter.app_name}}/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Build Stage
FROM {{cookiecutter.docker_build_image}}:{{cookiecutter.docker_build_image_version}} AS build-stage

LABEL app="build-{{cookiecutter.app_name}}"
LABEL app="{{cookiecutter.app_name}}"
LABEL REPO="https://{{cookiecutter.source_path}}/{{cookiecutter.app_name}}"

ARG GOPRIVATE={{cookiecutter.goprivate}}
Expand Down
46 changes: 45 additions & 1 deletion {{cookiecutter.app_name}}/Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: build build-alpine clean test default deps generate run run-docker fmt help bench build-docker coverage-html dep lint mock runj vulncheck
.PHONY: build build-alpine clean test default deps generate run run-docker fmt help bench build-docker coverage-html dep lint mock runj vulncheck local-stack local-stack-down local-stack-logs local-stack-rm local-stack-reset local-psql loadtest

BIN_NAME={{cookiecutter.app_name}}
GOPRIVATE ?= {{cookiecutter.goprivate}}
Expand Down Expand Up @@ -35,6 +35,12 @@ help:
@echo ' make run-docker Run the project in a docker container.'
@echo ' make runj Run the project locally with jq log parsing.'
@echo ' make test Run tests.'
@echo ' make loadtest Run gRPC load test (ghz) against the running service.'
@echo ' make local-stack Start local dev stack (PROFILES="deps" for db/redis, "deps obs" for full).'
@echo ' make local-stack-down Stop local dev stack.'
@echo ' make local-stack-logs Follow local dev stack logs.'
@echo ' make local-stack-reset Reset local dev stack (down + rm + up).'
@echo ' make local-psql Open psql shell in local Postgres.'
@echo

build:
Expand Down Expand Up @@ -100,3 +106,41 @@ runj: build

run-docker: build-docker
docker run -p 9091:9091 -p 9090:9090 --env-file local.env ${IMAGE_NAME}:local

# Local development stack (docker-compose)
# Usage:
# make local-stack PROFILES="deps" # postgres, redis, adminer
# make local-stack PROFILES="deps obs" # + prometheus, grafana
# make local-stack-down PROFILES="deps" # tear down with deps

COMPOSE_FILE := docker-compose.local.yml
PROFILE_FLAGS := $(foreach p,$(PROFILES),--profile $(p))

local-stack:
docker-compose -f $(COMPOSE_FILE) $(PROFILE_FLAGS) up -d --wait --remove-orphans
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
@echo
@echo "Local stack is running. Run 'make run' to start the app."
@echo "Endpoints:"
@docker-compose -f $(COMPOSE_FILE) $(PROFILE_FLAGS) port db 5432 2>/dev/null | sed 's|.*:\(.*\)| Postgres: localhost:\1|' || true
@docker-compose -f $(COMPOSE_FILE) $(PROFILE_FLAGS) port redis 6379 2>/dev/null | sed 's|.*:\(.*\)| Redis: localhost:\1|' || true
@docker-compose -f $(COMPOSE_FILE) $(PROFILE_FLAGS) port adminer 8080 2>/dev/null | sed 's|.*:\(.*\)| Adminer: http://localhost:\1|' || true
@docker-compose -f $(COMPOSE_FILE) $(PROFILE_FLAGS) port prometheus 9090 2>/dev/null | sed 's|.*:\(.*\)| Prometheus: http://localhost:\1|' || true
@docker-compose -f $(COMPOSE_FILE) $(PROFILE_FLAGS) port grafana 3000 2>/dev/null | sed 's|.*:\(.*\)| Grafana: http://localhost:\1 (admin/admin)|' || true
@echo

local-stack-down:
docker-compose -f $(COMPOSE_FILE) $(PROFILE_FLAGS) down

Comment thread
ankurs marked this conversation as resolved.
local-stack-logs:
docker-compose -f $(COMPOSE_FILE) $(PROFILE_FLAGS) logs -f

local-stack-rm:
docker-compose -f $(COMPOSE_FILE) $(PROFILE_FLAGS) rm

local-stack-reset: local-stack-down local-stack-rm local-stack
Comment thread
ankurs marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

local-psql:
docker exec -ti {{cookiecutter.app_name}}-db-1 bash -c 'PGPASSWORD=postgres psql -U postgres -d {{cookiecutter.app_name}}_dev'
Comment thread
ankurs marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

loadtest:
ghz --config misc/loadtest/echo.json
27 changes: 27 additions & 0 deletions {{cookiecutter.app_name}}/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,36 @@ The Makefile contains a number of useful commands to help you get started. Here
- `make lint` - Runs the linter
- `make run` - Runs the application
- `make runj` - Runs the application with json logs parsing with jq
- `make loadtest` - Runs gRPC load test ([ghz](https://ghz.sh)) against the running service
- `make build` - Builds the application
- `make generate` - Generates the code

## Local Development Stack

Start infrastructure dependencies with docker-compose, then run the app natively:

```console
$ make local-stack PROFILES="deps" # Start Postgres, Redis, Adminer
$ make local-stack PROFILES="deps obs" # + Prometheus, Grafana (with pre-built dashboard)
$ make run # Run the app (fast native build)
```

Infrastructure management:

```console
$ make local-stack-down PROFILES="deps" # Stop infrastructure
$ make local-stack-reset PROFILES="deps" # Reset infrastructure
$ make local-psql # Open Postgres shell
```

Endpoints when running with all profiles:
- **Swagger UI**: http://localhost:9091/swagger/
- **Grafana**: http://localhost:3000 (admin/admin) — ColdBrew dashboard pre-loaded
- **Prometheus**: http://localhost:9100
- **Adminer**: http://localhost:8088
- **Postgres**: localhost:5433
- **Redis**: localhost:6379

## Docker

This project also contains a Dockerfile to help you get started with Docker. To build the image, run:
Expand Down
Loading
Loading