15 Commits
Author SHA1 Message Date
vladp f4bcb58353 fix type GF_AUTH
Test Workflow / test (push) Successful in 2s
2025-12-08 15:30:07 +07:00
vladp 6a78475844 Merge remote-tracking branch 'origin/dev' into dev
Test Workflow / test (push) Successful in 1m26s
2025-12-01 11:39:27 +07:00
vladp 8a4b8e49f1 add test workflow 2025-12-01 11:39:15 +07:00
backdev 235ea7143a Удалить .gitea 2025-12-01 12:37:06 +08:00
backdev 520d2df3a9 Добавить .gitea 2025-12-01 12:34:24 +08:00
backdev 3b0803bef3 Удалить job.yaml 2025-12-01 12:33:50 +08:00
backdev 7d17b8d617 Обновить job.yaml 2025-12-01 12:32:06 +08:00
backdev c51608276d Добавить job 2025-12-01 12:31:34 +08:00
backdev f8c6c83c96 Merge pull request 'NCP-2 add logout' (#4) from NCP-2 into dev
Reviewed-on: #4
2025-11-26 12:52:54 +08:00
backdev 27ce3cc9ed Merge pull request 'INFRA-1 add grafana,loki,prometheus' (#3) from INFRA-1 into dev
Reviewed-on: #3
2025-11-26 12:51:30 +08:00
vladp 1d9d35d7c0 Merge branch 'dev' into INFRA-1 2025-11-26 11:48:04 +07:00
vladp e7481a53d7 add simple endpoint for users 2025-11-26 11:33:33 +07:00
vladp 82b9a8e166 add modules list 2025-11-25 14:27:12 +07:00
vladp adec554122 NCP-2 add logout 2025-11-25 14:20:28 +07:00
vladp c64d4b1b07 INFRA-1 add grafana,loki,prometheus 2025-11-25 13:31:50 +07:00
19 changed files with 743 additions and 7 deletions
+17
View File
@@ -0,0 +1,17 @@
name: Test Workflow
on:
push:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Print environment variables
run: |
echo "GITHUB_REPOSITORY: $GITHUB_REPOSITORY"
echo "GITHUB_SHA: $GITHUB_SHA"
echo "GITHUB_REF: $GITHUB_REF"
echo "GITHUB_ACTOR: $GITHUB_ACTOR"
echo "PATH: $PATH"
+36
View File
@@ -0,0 +1,36 @@
### MODULES
-api
- register +
- logut +
- login +
- check-demo-status
- check-token-balance
- content
- dismiss-notification
- download
- download-certificate
- download-pdf-tracked
- export-report
- forgor-password
- generate-certificate
- get-pdf-tracking-token
- get-user-data
- login ?
- logaut ?
- notifications-api
- preview-certificate
- register
- reports
- reset-password
- token-manager
- track-pdf-open
- upload
- validate-upload
- verify-video
- violations
infra
-grafana/prom +
- ci/cd
+94 -2
View File
@@ -15,7 +15,8 @@ services:
container_name: postgres container_name: postgres
app: app:
image: popovtsev/ncp # image: popovtsev/ncp
build: .
container_name: no_copy_app container_name: no_copy_app
environment: environment:
POSTGRES_DB: no_copy_ POSTGRES_DB: no_copy_
@@ -28,5 +29,96 @@ services:
ports: ports:
- "8080:8080" - "8080:8080"
grafana:
image: grafana/grafana:10.3.1
container_name: grafana
ports:
- "3000:3000"
depends_on:
prometheus:
condition: service_healthy
loki:
condition: service_healthy
tempo:
condition: service_started
alloy:
condition: service_started
environment:
GF_AUTH_ANONYMOUS_ENABLED: true
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: admin
GF_METRICS_ENABLED: "true"
volumes:
- ./infrastructure/grafana/provisioning:/etc/grafana/provisioning
- ./infrastructure/grafana/dashboards:/var/lib/grafana/dashboards
healthcheck:
test: [ "CMD", "wget", "--spider", "http://localhost:3000/api/health" ]
interval: 10s
timeout: 5s
retries: 10
prometheus:
image: prom/prometheus:latest
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./infrastructure/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
healthcheck:
test: [ "CMD", "wget", "--spider", "http://localhost:9090/-/healthy" ]
interval: 10s
timeout: 5s
retries: 5
loki:
image: grafana/loki:2.9.2
container_name: loki
ports:
- "3100:3100"
volumes:
- ./infrastructure/loki/loki-config.yaml:/etc/loki/loki-config.yaml:ro
healthcheck:
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3100/ready" ]
interval: 10s
timeout: 5s
retries: 5
tempo:
image: grafana/tempo:2.4.1
container_name: tempo
command: [ "-config.file=/etc/tempo/tempo.yaml" ]
volumes:
- ./infrastructure/tempo/tempo.yaml:/etc/tempo/tempo.yaml
- tempo_data:/var/tempo
ports:
- "3200:3200"
alloy:
image: grafana/alloy:latest
container_name: alloy
user: root
ports:
- "9080:9080" # HTTP interface
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP http
volumes:
- ./infrastructure/alloy/config.alloy:/etc/alloy/config.alloy:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock
- /var/log:/var/log:ro
environment:
GRAFANA_LOKI_URL: http://loki:3100/loki/api/v1/push
command:
- run
- --server.http.listen-addr=0.0.0.0:9080
- --storage.path=/var/lib/alloy/data
- /etc/alloy/config.alloy
volumes: volumes:
pgdata: pgdata:
artifacts:
tempo_data:
loki_data:
loki_chunks:
loki_index:
loki_rules:
+74
View File
@@ -0,0 +1,74 @@
loki.write "local" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}
discovery.docker "containers" {
host = "unix:///var/run/docker.sock"
refresh_interval = "5s"
}
discovery.relabel "containers" {
targets = discovery.docker.containers.targets
rule {
source_labels = ["__meta_docker_container_name"]
regex = "/(.*)"
target_label = "container"
}
rule {
target_label = "job"
replacement = "docker"
}
rule {
target_label = "host"
replacement = constants.hostname
}
rule {
source_labels = ["__meta_docker_container_name"]
regex = "/(.*)"
target_label = "service"
}
rule {
source_labels = ["__meta_docker_container_log_stream"]
target_label = "stream"
}
}
loki.source.docker "containers" {
host = "unix:///var/run/docker.sock"
targets = discovery.docker.containers.targets
relabel_rules = discovery.relabel.containers.rules
forward_to = [loki.write.local.receiver]
}
otelcol.receiver.otlp "default" {
grpc {
endpoint = "0.0.0.0:4317"
}
http {
endpoint = "0.0.0.0:4318"
}
output {
traces = [otelcol.exporter.otlp.tempo.input]
metrics = [otelcol.exporter.prometheus.default.input]
}
}
otelcol.exporter.otlp "tempo" {
client {
endpoint = "tempo:4317"
tls {
insecure = true
}
}
}
otelcol.exporter.prometheus "default" {
forward_to = []
}
@@ -0,0 +1,43 @@
{
"title": "Postgres Overview",
"timezone": "browser",
"schemaVersion": 39,
"version": 1,
"refresh": "10s",
"panels": [
{
"type": "gauge",
"title": "pg_up",
"targets": [{ "expr": "pg_up", "refId": "A" }],
"gridPos": { "x": 0, "y": 0, "w": 4, "h": 6 }
},
{
"type": "timeseries",
"title": "Active sessions by state",
"targets": [
{ "expr": "sum by (state,instance) (pg_stat_activity_count)", "refId": "A" }
],
"gridPos": { "x": 4, "y": 0, "w": 20, "h": 6 }
},
{
"type": "timeseries",
"title": "Transactions per second (commits+rollbacks)",
"targets": [
{ "expr": "sum(rate(pg_stat_database_xact_commit[1m])) + sum(rate(pg_stat_database_xact_rollback[1m]))", "refId": "A" }
],
"gridPos": { "x": 0, "y": 6, "w": 12, "h": 8 }
},
{
"type": "timeseries",
"title": "Tuples fetched/returned/inserted/updated/deleted",
"targets": [
{ "expr": "sum(rate(pg_stat_database_tup_fetched[1m]))", "legendFormat": "fetched", "refId": "A" },
{ "expr": "sum(rate(pg_stat_database_tup_returned[1m]))", "legendFormat": "returned", "refId": "B" },
{ "expr": "sum(rate(pg_stat_database_tup_inserted[1m]))", "legendFormat": "inserted", "refId": "C" },
{ "expr": "sum(rate(pg_stat_database_tup_updated[1m]))", "legendFormat": "updated", "refId": "D" },
{ "expr": "sum(rate(pg_stat_database_tup_deleted[1m]))", "legendFormat": "deleted", "refId": "E" }
],
"gridPos": { "x": 12, "y": 6, "w": 12, "h": 8 }
}
]
}
@@ -0,0 +1,20 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: false
editable: true
- name: Tempo
type: tempo
access: proxy
url: http://tempo:3200
isDefault: false
editable: true
@@ -0,0 +1,11 @@
apiVersion: 1
providers:
- name: 'default'
folder: 'Individuals-api'
type: file
disableDeletion: false
editable: true
options:
path: /var/lib/grafana/dashboards
foldersFromFilesStructure: true
@@ -0,0 +1,20 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: false
editable: true
- name: Tempo
type: tempo
access: proxy
url: http://tempo:3200
isDefault: false
editable: true
@@ -0,0 +1,218 @@
pipeline {
agent any
parameters {
string(
name: 'BRANCH',
defaultValue: 'dev',
description: 'Ветка для деплоя'
)
string(
name: 'SERVER',
defaultValue: '92.242.61.23',
description: 'Сервер для деплоя'
)
}
stages {
stage('Git pull') {
steps {
script {
checkout([
$class: 'GitSCM',
branches: [[name: params.BRANCH]],
userRemoteConfigs: [[
url: 'https://code.3err0.ru/backdev/no-copy.git',
credentialsId: 'nx-jen'
]]
])
}
}
}
stage('Stop old container') {
steps {
script {
withCredentials([
usernamePassword(
credentialsId: 'server-root-password',
usernameVariable: 'SSH_USER',
passwordVariable: 'SSH_PASS'
)
]) {
sh """
echo "Stop old container by branch ${params.BRANCH}..."
sshpass -p '$SSH_PASS' ssh $SSH_USER@$SERVER "
cd /opt/deployments/${params.BRANCH} 2>/dev/null || mkdir -p /opt/deployments/${params.BRANCH}
# Stop old container if have docker-compose
if [ -f 'docker-compose.yaml' ] || [ -f 'docker-compose.yml' ]; then
if [ -f 'docker-compose.yaml' ]; then
docker-compose -f docker-compose.yaml down 2>/dev/null || true
elif [ -f 'docker-compose.yml' ]; then
docker-compose -f docker-compose.yml down 2>/dev/null || true
fi
fi
# Stop and delete container by name
docker stop app-${params.BRANCH} 2>/dev/null || true
docker rm app-${params.BRANCH} 2>/dev/null || true
echo "Old containder deleted"
"
"""
}
}
}
}
stage('Copy to server') {
steps {
script {
withCredentials([
usernamePassword(
credentialsId: 'server-root-password',
usernameVariable: 'SSH_USER',
passwordVariable: 'SSH_PASS'
)
]) {
sh """
echo "Copy files on server..."
# Clean mkdir
sshpass -p '$SSH_PASS' ssh $SSH_USER@$SERVER "
rm -rf /opt/deployments/${params.BRANCH}/*
mkdir -p /opt/deployments/${params.BRANCH}
"
# Copy all files
sshpass -p '$SSH_PASS' scp -r ./* $SSH_USER@$SERVER:/opt/deployments/${params.BRANCH}/ 2>/dev/null || true
echo "Files copied"
"""
}
}
}
}
stage('Build on server') {
steps {
script {
withCredentials([
usernamePassword(
credentialsId: 'server-root-password',
usernameVariable: 'SSH_USER',
passwordVariable: 'SSH_PASS'
)
]) {
sh """
echo "Build on server..."
sshpass -p '$SSH_PASS' ssh $SSH_USER@$SERVER "
cd /opt/deployments/${params.BRANCH}
echo "Build Docker for branch ${params.BRANCH}..."
# delete old docker image
docker rmi app:${params.BRANCH} 2>/dev/null || true
# build from branch
if [ -f 'Dockerfile' ]; then
docker build -t app:${params.BRANCH} .
echo "Docker образ app:${params.BRANCH} собран"
fi
# or from docker-compose
if [ -f 'docker-compose.yaml' ]; then
docker-compose -f docker-compose.yaml build
echo "Build from docker-compose.yaml completed"
elif [ -f 'docker-compose.yml' ]; then
docker-compose -f docker-compose.yml build
echo "Build from docker-compose.yml completed"
fi
echo 'Build completed'
"
"""
}
}
}
}
stage('Start on server') {
steps {
script {
withCredentials([
usernamePassword(
credentialsId: 'server-root-password',
usernameVariable: 'SSH_USER',
passwordVariable: 'SSH_PASS'
)
]) {
sh """
echo "Start container..."
sshpass -p '$SSH_PASS' ssh $SSH_USER@$SERVER "
cd /opt/deployments/${params.BRANCH}
# If have docker-compose - use them
if [ -f 'docker-compose.yaml' ]; then
echo "Start docker-compose.yaml..."
docker-compose -f docker-compose.yaml up -d
elif [ -f 'docker-compose.yml' ]; then
echo "Start docker-compose.yml..."
docker-compose -f docker-compose.yml up -d
# If have Dockerfile - start
elif [ -f 'Dockerfile' ]; then
echo "Start app-${params.BRANCH}..."
docker run -d \\
--name app-${params.BRANCH} \\
-p 8080:8080 \\
app:${params.BRANCH}
else
echo "Error:Dockerfile NOT FOUND, docker-compose.yaml or docker-compose.yml"
exit 1
fi
echo 'Container start'
# Wait start
sleep 10
# Check status
echo 'status container:'
if [ -f 'docker-compose.yaml' ] || [ -f 'docker-compose.yml' ]; then
if [ -f 'docker-compose.yaml' ]; then
docker-compose -f docker-compose.yaml ps
else
docker-compose -f docker-compose.yml ps
fi
else
docker ps --filter "name=app-${params.BRANCH}"
fi
echo ''
echo 'Apps start on:'
echo 'http://$SERVER:80'
"
"""
}
}
}
}
}
post {
success {
echo "Deploy branch ${params.BRANCH} completed"
echo "App : http://$SERVER:80"
}
failure {
echo "Deploy failed"
}
}
}
+56
View File
@@ -0,0 +1,56 @@
auth_enabled: false
server:
http_listen_post: 3100
ingester:
lifecycler:
address: 0.0.0.0
ring:
kvstore:
store: summary
replication_factor: 1
final_sleep: 0s
chunk_idle_period: 5m
chunk_retain_period: 30s
max_transfer_retries: 0
schema_config:
configs:
- from: 2018-04-15
store: boltdb
object_store: filesystem
schema: v11
index:
prefix: index_
period: 168h
storage_config:
boltdb:
directory: /loki/index
filesystem:
directory: /loki/chunks
limits_config:
enforce_metric_name: false
reject_old_samples: true
reject_old_samples_max_age: 168h
ingestion_rate_mb: 8
ingestion_burst_size_md: 10
chunk_storage_config:
max_look_back_period: 0s
table_manager:
chunk_tables_provisioning:
inactive_read_throughput: 0
inactive_write_throughput: 0
provisioned_read_throughput: 0
provisioned_write_throughput: 0
index_tables_provisioning:
inactive_read_throughput: 0
inactive_write_throughput: 0
provisioned_read_throughput: 0
provisioned_write_throughput: 0
retention_deletes_enabled: true
retention_period: 672h
+53
View File
@@ -0,0 +1,53 @@
global:
scrape_interval: 15s
scrape_timeout: 10s
evaluation_interval: 15s
scrape_configs:
- job_name: prometheus
static_configs:
- targets: [ "prometheus:9090" ]
- job_name: api
metrics_path: /actuator/prometheus
static_configs:
- targets: [ 'api:8091' ]
- job_name: persons-api
metrics_path: /actuator/prometheus
static_configs:
- targets: [ 'persons-api:8092' ]
- job_name: keycloak
metrics_path: /metrics
static_configs:
- targets: [ 'keycloak:9000' ]
- job_name: grafana
metrics_path: /metrics
static_configs:
- targets: [ 'grafana:3000' ]
- job_name: alloy
metrics_path: /metrics
static_configs:
- targets: [ 'alloy:9080' ]
- job_name: loki
metrics_path: /metrics
static_configs:
- targets: [ 'loki:3100' ]
- job_name: tempo
metrics_path: /metrics
static_configs:
- targets: [ 'tempo:3200' ]
- job_name: person-postgres
static_configs:
- targets: [ 'person-postgres-exporter:9187' ]
- job_name: keycloak-postgres
static_configs:
- targets: [ 'keycloak-postgres-exporter:9187' ]
+22
View File
@@ -0,0 +1,22 @@
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
http:
ingester:
trace_idle_period: 10s
compactor:
compaction:
compacted_block_retention: 48h
storage:
trace:
backend: local
local:
path: /var/tempo/traces
@@ -3,10 +3,7 @@ package ru.soune.no_copy.controller;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.soune.no_copy.dto.AuthResponse; import ru.soune.no_copy.dto.AuthResponse;
import ru.soune.no_copy.dto.LoginRequest; import ru.soune.no_copy.dto.LoginRequest;
import ru.soune.no_copy.dto.LoginResponse; import ru.soune.no_copy.dto.LoginResponse;
@@ -14,6 +11,8 @@ import ru.soune.no_copy.dto.RegisterRequest;
import ru.soune.no_copy.entity.AuthToken; import ru.soune.no_copy.entity.AuthToken;
import ru.soune.no_copy.service.AuthService; import ru.soune.no_copy.service.AuthService;
import java.util.Map;
@RestController @RestController
@RequestMapping("/api/auth") @RequestMapping("/api/auth")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -36,4 +35,13 @@ public class AuthController {
return ResponseEntity.ok(new LoginResponse(true, login.getUser().getEmail(), return ResponseEntity.ok(new LoginResponse(true, login.getUser().getEmail(),
login.getToken(),login.getExpiresAt().toString())); login.getToken(),login.getExpiresAt().toString()));
} }
@PostMapping("/logout")
public ResponseEntity<?> logout(@RequestHeader("Authorization") String tokenHeader) {
String token = tokenHeader.replace("Bearer ", "");
authService.logout(token);
return ResponseEntity.ok(Map.of("success", true));
}
} }
@@ -0,0 +1,28 @@
package ru.soune.no_copy.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.soune.no_copy.dto.UserDTO;
import ru.soune.no_copy.repository.UserRepository;
import java.util.List;
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserController {
private final UserRepository userRepository;
@GetMapping("/all")
public ResponseEntity<List<UserDTO>> getAllUsers() {
List<UserDTO> allUsers = userRepository.findAll().stream()
.map(u -> new UserDTO(u.getFirstName(), u.getEmail(), u.getIsActive()))
.toList();
return ResponseEntity.ok(allUsers);
}
}
@@ -0,0 +1,12 @@
package ru.soune.no_copy.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class UserDTO {
private String fullName;
private String email;
private boolean isActive;
}
@@ -53,6 +53,6 @@ public class User {
private LocalDateTime createdAt; private LocalDateTime createdAt;
@Column(name = "is_active") @Column(name = "is_active")
private Boolean isActive; private Boolean isActive = true;
} }
@@ -10,4 +10,5 @@ import java.util.Optional;
@Repository @Repository
public interface AuthTokenRepository extends JpaRepository<AuthToken, Long> { public interface AuthTokenRepository extends JpaRepository<AuthToken, Long> {
Optional<AuthToken> findByTokenAndExpiresAtAfter(String token, LocalDate expiresAtAfter); Optional<AuthToken> findByTokenAndExpiresAtAfter(String token, LocalDate expiresAtAfter);
Optional<AuthToken> findByToken(String token);
} }
@@ -78,6 +78,10 @@ public class AuthService {
return authTokenRepository.save(authToken); return authTokenRepository.save(authToken);
} }
public void logout(String token) {
authTokenRepository.findByToken(token)
.ifPresent(authTokenRepository::delete);
}
private String generateAuthToken() { private String generateAuthToken() {
byte[] bytes = new byte[32]; byte[] bytes = new byte[32];
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration>
<configuration scan="true">
<statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>{"timestamp":"%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}", "trace_id": "%X{trace_id}",
"span_id":"%X{span_id}", "level":"%level", "thread":"%thread",
"logger":"%logger{36}","message":"%msg"}%n
</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="console"/>
</root>
</configuration>