Skip to content

Commit 136a451

Browse files
Implement all open issues (+ Chimney 2.0.0-RC1) (#33)
Closes #3,#4,#7,#8,#9,#10,#11,#14,#18,#19,#21,#22,#23,#25,#29,#30. #28 dropped. Full suite green (108 examples, 0 failures). NoPermission->403; auth errors mapped (were 500); Chimney 2.0.0-RC1; Postgres 17.
1 parent 62ffefb commit 136a451

104 files changed

Lines changed: 3950 additions & 301 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ intended for example app:
4949
Building and testing requires java installed. Then
5050

5151
```bash
52-
./sbt
52+
sbt
5353
```
5454

5555
downloads and runs sbt shell.
@@ -61,10 +61,10 @@ just dev-bg # starts services in background
6161
just dev-up # starts services in terminal (I suggest a separate tab/window)
6262
```
6363

64-
Then it is possible to run it tests:
64+
Then it is possible to run integration tests:
6565

6666
```bash
67-
sbt> it:test
67+
sbt> fullTest
6868
```
6969

7070
```bash

branchtalk.sbt

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ lazy val root = project
164164
.aggregate(commonInfrastructure)
165165
.aggregate(discussions.projectRefs *)
166166
.aggregate(discussionsApi.projectRefs *)
167+
.aggregate(notifications.projectRefs *)
168+
.aggregate(notificationsApi.projectRefs *)
169+
.aggregate(notificationsImpl)
167170
.aggregate(users.projectRefs *)
168171
.aggregate(usersApi.projectRefs *)
169172
.aggregate(usersImpl)
@@ -390,6 +393,63 @@ val usersImpl = project
390393
users.jvm(Dependencies.scalaVersion)
391394
)
392395

396+
// notifications
397+
398+
val notifications = projectMatrix
399+
.in(file("modules/notifications"))
400+
.jvmPlatform(List(Dependencies.scalaVersion))
401+
.enablePlugins(GitVersioning, GitBranchPrompt)
402+
.settings(
403+
name := "notifications",
404+
description := "Notifications' published language"
405+
)
406+
.settings(settings)
407+
.settings(tests)
408+
.settings(
409+
libraryDependencies ++= Seq(
410+
Dependencies.fs2
411+
),
412+
customPredef("scala.util.chaining", "cats.implicits", "neotype")
413+
)
414+
.dependsOn(common)
415+
416+
val notificationsApi = projectMatrix
417+
.in(file("modules/notifications-api"))
418+
.jvmPlatform(List(Dependencies.scalaVersion))
419+
.enablePlugins(GitVersioning, GitBranchPrompt)
420+
.settings(
421+
name := "notifications-api",
422+
description := "Notifications' HTTP API"
423+
)
424+
.settings(settings)
425+
.settings(tests)
426+
.settings(
427+
libraryDependencies ++= Seq(
428+
Dependencies.kindlingsJsoniter
429+
),
430+
customPredef("scala.util.chaining", "cats.implicits", "neotype")
431+
)
432+
.dependsOn(commonApi, notifications)
433+
434+
val notificationsImpl = project
435+
.in(file("modules/notifications-impl"))
436+
.enablePlugins(GitVersioning, GitBranchPrompt)
437+
.settings(
438+
name := "notifications-impl",
439+
description := "Notifications' Reads, Writes and Services' implementations"
440+
)
441+
.settings(settings)
442+
.settings(integrationTests)
443+
.settings(
444+
customPredef("scala.util.chaining", "cats.implicits", "neotype")
445+
)
446+
.dependsOn(
447+
common.jvm(Dependencies.scalaVersion) % s"$Compile->$Compile ; $Test->$Test",
448+
commonInfrastructure % s"$Compile->$Compile ; $Test->$Test",
449+
discussions.jvm(Dependencies.scalaVersion),
450+
notifications.jvm(Dependencies.scalaVersion)
451+
)
452+
393453
// application
394454

395455
val server = project
@@ -435,6 +495,9 @@ val server = project
435495
discussions.jvm(Dependencies.scalaVersion),
436496
discussionsApi.jvm(Dependencies.scalaVersion),
437497
discussionsImpl % s"$Test->$Test",
498+
notifications.jvm(Dependencies.scalaVersion),
499+
notificationsApi.jvm(Dependencies.scalaVersion),
500+
notificationsImpl % s"$Test->$Test",
438501
users.jvm(Dependencies.scalaVersion),
439502
usersApi.jvm(Dependencies.scalaVersion),
440503
usersImpl % s"$Test->$Test"
@@ -480,7 +543,7 @@ val application = project
480543
},
481544
assembly / mainClass := Some("io.branchtalk.Main")
482545
)
483-
.dependsOn(server, discussionsImpl, usersImpl)
546+
.dependsOn(server, discussionsImpl, notificationsImpl, usersImpl)
484547

485548
// aliases
486549

docker-compose/filebeat.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Filebeat configuration — ships Docker container logs to Elasticsearch.
2+
# Mounted into the filebeat container by monolith-monitoring.yml.
3+
#
4+
# The application's logback already emits JSON to stdout, so Filebeat only
5+
# needs to pick up the container log files and forward them.
6+
7+
filebeat.autodiscover:
8+
providers:
9+
- type: docker
10+
hints.enabled: true
11+
# Only collect logs from the branchtalk application container.
12+
templates:
13+
- condition:
14+
contains:
15+
docker.container.image: "branchtalk-server"
16+
config:
17+
- type: container
18+
paths:
19+
- "/var/lib/docker/containers/${data.docker.container.id}/*.log"
20+
# The app writes JSON; Docker wraps each line in its own JSON
21+
# envelope. Decode the Docker layer, then parse the inner JSON
22+
# that logback produced.
23+
processors:
24+
- decode_json_fields:
25+
fields: ["message"]
26+
target: "app"
27+
overwrite_keys: true
28+
29+
output.elasticsearch:
30+
hosts: ["elasticsearch:9200"]
31+
indices:
32+
- index: "branchtalk-logs-%{+yyyy.MM.dd}"
33+
34+
setup.ilm.enabled: false
35+
setup.template.name: "branchtalk-logs"
36+
setup.template.pattern: "branchtalk-logs-*"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Auto-provision Prometheus as the default Grafana data source.
2+
# Mounted into grafana at /etc/grafana/provisioning/datasources/ by monolith-monitoring.yml.
3+
4+
apiVersion: 1
5+
6+
datasources:
7+
- name: Prometheus
8+
type: prometheus
9+
access: proxy
10+
url: http://prometheus:9090
11+
isDefault: true
12+
editable: true

docker-compose/monolith-deps.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ networks:
66

77
services:
88
postgres:
9-
image: 'postgres:12-alpine'
9+
image: 'postgres:17-alpine'
1010
restart: always
1111
ports:
1212
- '127.0.0.1:5432:5432'
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Opt-in monitoring and log-aggregation stack for local development.
2+
#
3+
# Start alongside the app dependencies:
4+
#
5+
# docker compose \
6+
# -f docker-compose/monolith-deps.yml \
7+
# -f docker-compose/monolith-setup.yml \
8+
# -f docker-compose/monolith-monitoring.yml \
9+
# up -d
10+
#
11+
# Then (optionally) the app itself:
12+
#
13+
# docker compose -f docker-compose/monolith-app.yml up -d
14+
#
15+
# Or run the app via sbt and point Prometheus at host.docker.internal:8080.
16+
#
17+
# UIs:
18+
# Prometheus — http://localhost:9090
19+
# Grafana — http://localhost:3000 (admin / admin)
20+
# Kibana — http://localhost:5601
21+
#
22+
# Log shipping:
23+
# The Filebeat sidecar reads the application container's JSON stdout logs
24+
# and forwards them to Elasticsearch. If you run the app outside Docker
25+
# (e.g. via sbt), pipe its output to a file and mount that file into the
26+
# filebeat container, or configure a LogstashTcpSocketAppender in logback.xml
27+
# (requires adding the logstash-logback-encoder dependency — see the
28+
# commented-out appender in modules/app/src/main/resources/logback.xml).
29+
30+
networks:
31+
branchtalk-monolith:
32+
driver: bridge
33+
34+
services:
35+
36+
# ---------- Metrics ----------
37+
38+
prometheus:
39+
image: prom/prometheus:v2.53.0
40+
restart: unless-stopped
41+
ports:
42+
- "127.0.0.1:9090:9090"
43+
volumes:
44+
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
45+
- prometheus_data:/prometheus
46+
command:
47+
- "--config.file=/etc/prometheus/prometheus.yml"
48+
- "--storage.tsdb.retention.time=7d"
49+
networks:
50+
- branchtalk-monolith
51+
52+
grafana:
53+
image: grafana/grafana:11.1.0
54+
restart: unless-stopped
55+
ports:
56+
- "127.0.0.1:3000:3000"
57+
environment:
58+
GF_SECURITY_ADMIN_USER: admin
59+
GF_SECURITY_ADMIN_PASSWORD: admin
60+
volumes:
61+
- ./grafana/provisioning:/etc/grafana/provisioning:ro
62+
- grafana_data:/var/lib/grafana
63+
depends_on:
64+
- prometheus
65+
networks:
66+
- branchtalk-monolith
67+
68+
# ---------- Log aggregation ----------
69+
70+
elasticsearch:
71+
image: docker.elastic.co/elasticsearch/elasticsearch:8.14.1
72+
restart: unless-stopped
73+
ports:
74+
- "127.0.0.1:9200:9200"
75+
environment:
76+
discovery.type: single-node
77+
xpack.security.enabled: "false" # dev-only — no auth
78+
xpack.security.http.ssl.enabled: "false"
79+
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
80+
volumes:
81+
- es_data:/usr/share/elasticsearch/data
82+
healthcheck:
83+
test: ["CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health || exit 1"]
84+
interval: 10s
85+
timeout: 10s
86+
retries: 10
87+
networks:
88+
- branchtalk-monolith
89+
90+
kibana:
91+
image: docker.elastic.co/kibana/kibana:8.14.1
92+
restart: unless-stopped
93+
ports:
94+
- "127.0.0.1:5601:5601"
95+
environment:
96+
ELASTICSEARCH_HOSTS: http://elasticsearch:9200
97+
depends_on:
98+
elasticsearch:
99+
condition: service_healthy
100+
networks:
101+
- branchtalk-monolith
102+
103+
# Filebeat reads the application container's stdout (JSON) and ships to ES.
104+
# If the application container is not running, Filebeat idles harmlessly.
105+
filebeat:
106+
image: docker.elastic.co/beats/filebeat:8.14.1
107+
restart: unless-stopped
108+
user: root # needs access to docker socket / log files
109+
volumes:
110+
- ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
111+
- /var/lib/docker/containers:/var/lib/docker/containers:ro
112+
- /var/run/docker.sock:/var/run/docker.sock:ro
113+
- filebeat_data:/usr/share/filebeat/data
114+
depends_on:
115+
elasticsearch:
116+
condition: service_healthy
117+
networks:
118+
- branchtalk-monolith
119+
120+
volumes:
121+
prometheus_data:
122+
driver: local
123+
grafana_data:
124+
driver: local
125+
es_data:
126+
driver: local
127+
filebeat_data:
128+
driver: local

docker-compose/monolith-setup.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ networks:
66

77
services:
88
postgres-setup:
9-
image: 'postgres:12-alpine'
9+
image: 'postgres:17-alpine'
1010
command: >
1111
sh -c "(psql --host postgres --username postgres --command=\"CREATE DATABASE users\" || true) &&
1212
(psql --host postgres --username postgres --command=\"CREATE USER users WITH PASSWORD 'password'\" users || true) &&
1313
(psql --host postgres --username postgres --command=\"CREATE SCHEMA users AUTHORIZATION users\" users || true) &&
1414
(psql --host postgres --username postgres --command=\"CREATE DATABASE discussions\" || true) &&
1515
(psql --host postgres --username postgres --command=\"CREATE USER discussions WITH PASSWORD 'password'\" discussions || true) &&
16-
(psql --host postgres --username postgres --command=\"CREATE SCHEMA discussions AUTHORIZATION discussions\" discussions || true)"
16+
(psql --host postgres --username postgres --command=\"CREATE SCHEMA discussions AUTHORIZATION discussions\" discussions || true) &&
17+
(psql --host postgres --username postgres --command=\"CREATE DATABASE notifications\" || true) &&
18+
(psql --host postgres --username postgres --command=\"CREATE USER notifications WITH PASSWORD 'password'\" notifications || true) &&
19+
(psql --host postgres --username postgres --command=\"CREATE SCHEMA notifications AUTHORIZATION notifications\" notifications || true)"
1720
environment:
1821
PGPASSWORD: password # for psql
1922
depends_on:
@@ -28,7 +31,9 @@ services:
2831
sh -c '/opt/kafka/bin/kafka-topics.sh --create --if-not-exists --bootstrap-server kafka:9094 --partitions 1 --replication-factor 1 --topic users &&
2932
/opt/kafka/bin/kafka-topics.sh --create --if-not-exists --bootstrap-server kafka:9094 --partitions 1 --replication-factor 1 --topic users-internal &&
3033
/opt/kafka/bin/kafka-topics.sh --create --if-not-exists --bootstrap-server kafka:9094 --partitions 1 --replication-factor 1 --topic discussions &&
31-
/opt/kafka/bin/kafka-topics.sh --create --if-not-exists --bootstrap-server kafka:9094 --partitions 1 --replication-factor 1 --topic discussions-internal'
34+
/opt/kafka/bin/kafka-topics.sh --create --if-not-exists --bootstrap-server kafka:9094 --partitions 1 --replication-factor 1 --topic discussions-internal &&
35+
/opt/kafka/bin/kafka-topics.sh --create --if-not-exists --bootstrap-server kafka:9094 --partitions 1 --replication-factor 1 --topic notifications &&
36+
/opt/kafka/bin/kafka-topics.sh --create --if-not-exists --bootstrap-server kafka:9094 --partitions 1 --replication-factor 1 --topic notifications-internal'
3237
depends_on:
3338
kafka:
3439
condition: service_healthy

docker-compose/prometheus.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Prometheus scrape configuration for branchtalk monitoring stack.
2+
# Mounted into the prometheus container by monolith-monitoring.yml.
3+
4+
global:
5+
scrape_interval: 15s
6+
evaluation_interval: 15s
7+
8+
scrape_configs:
9+
- job_name: "branchtalk"
10+
# The application exposes GET /metrics (Prometheus text format 0.0.4).
11+
# When running inside Docker, "application" resolves via the compose network;
12+
# when scraping a host-local app, use host.docker.internal:8080 instead.
13+
static_configs:
14+
- targets: ["application:8080"]
15+
metrics_path: "/metrics"

0 commit comments

Comments
 (0)