hangar health report

This commit is contained in:
Jake Potrebic 2020-08-03 15:33:13 -07:00
parent e53475cc8e
commit d2448a1cfa
No known key found for this signature in database
GPG Key ID: 7C58557EC9C421F8
8 changed files with 187 additions and 50 deletions

View File

@ -3,6 +3,8 @@ package me.minidigger.hangar.config.hangar;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
@ConfigurationProperties(prefix = "hangar.projects")
public class ProjectsConfig {
@ -13,7 +15,7 @@ public class ProjectsConfig {
private int initVersionLoad = 10;
private int maxDescLen = 120;
private boolean fileValidate = true;
private String staleAge = "28d";
private Duration staleAge = Duration.ofDays(28);
private String checkInterval = "1h";
private String draftExpire = "1d";
private int userGridPageSize = 30;
@ -74,11 +76,11 @@ public class ProjectsConfig {
this.fileValidate = fileValidate;
}
public String getStaleAge() {
public Duration getStaleAge() {
return staleAge;
}
public void setStaleAge(String staleAge) {
public void setStaleAge(Duration staleAge) {
this.staleAge = staleAge;
}

View File

@ -3,10 +3,13 @@ package me.minidigger.hangar.controller;
import me.minidigger.hangar.db.customtypes.LoggedActionType;
import me.minidigger.hangar.db.customtypes.LoggedActionType.ProjectContext;
import me.minidigger.hangar.model.NamedPermission;
import me.minidigger.hangar.model.Visibility;
import me.minidigger.hangar.model.viewhelpers.ProjectFlag;
import me.minidigger.hangar.model.viewhelpers.ReviewQueueEntry;
import me.minidigger.hangar.model.viewhelpers.UnhealthyProject;
import me.minidigger.hangar.model.viewhelpers.UserData;
import me.minidigger.hangar.security.annotations.GlobalPermission;
import me.minidigger.hangar.service.JobService;
import me.minidigger.hangar.service.UserActionLogService;
import me.minidigger.hangar.service.UserService;
import me.minidigger.hangar.service.VersionService;
@ -31,6 +34,7 @@ import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Controller
public class ApplicationController extends HangarController {
@ -40,14 +44,16 @@ public class ApplicationController extends HangarController {
private final FlagService flagService;
private final UserActionLogService userActionLogService;
private final VersionService versionService;
private final JobService jobService;
@Autowired
public ApplicationController(UserService userService, ProjectService projectService, VersionService versionService, FlagService flagService, UserActionLogService userActionLogService) {
public ApplicationController(UserService userService, ProjectService projectService, VersionService versionService, FlagService flagService, UserActionLogService userActionLogService, JobService jobService) {
this.userService = userService;
this.projectService = projectService;
this.flagService = flagService;
this.userActionLogService = userActionLogService;
this.versionService = versionService;
this.jobService = jobService;
}
@RequestMapping("/")
@ -117,10 +123,19 @@ public class ApplicationController extends HangarController {
userActionLogService.project(request, LoggedActionType.PROJECT_FLAG_RESOLVED.with(ProjectContext.of(flag.getFlag().getProjectId())), "Flag resovled by " + userName, "Flagged by " + flag.getReportedBy());
}
@GlobalPermission(NamedPermission.VIEW_HEALTH)
@Secured("ROLE_USER")
@RequestMapping("/admin/health")
public ModelAndView showHealth() {
return fillModel(new ModelAndView("users/admin/health")); // TODO implement showHealth request controller
ModelAndView mav = new ModelAndView("users/admin/health");
List<UnhealthyProject> unhealthyProjects = projectService.getUnhealthyProjects();
mav.addObject("noTopicProjects", unhealthyProjects.stream().filter(p -> p.getTopicId() == null || p.getPostId() == null).collect(Collectors.toList()));
mav.addObject("staleProjects", unhealthyProjects);
mav.addObject("notPublicProjects", unhealthyProjects.stream().filter(p -> p.getVisibility() != Visibility.PUBLIC).collect(Collectors.toList()));
// TODO missingFiles
mav.addObject("missingFileProjects");
mav.addObject("erroredJobs", jobService.getErroredJobs());
return fillModel(mav);
}
@Secured("ROLE_USER")

View File

@ -0,0 +1,16 @@
package me.minidigger.hangar.db.dao;
import me.minidigger.hangar.db.model.JobsTable;
import org.jdbi.v3.sqlobject.config.RegisterBeanMapper;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
@RegisterBeanMapper(JobsTable.class)
public interface JobDao {
@SqlQuery("SELECT * FROM jobs WHERE state = 'fatal_failure'")
List<JobsTable> getErroredJobs();
}

View File

@ -9,15 +9,19 @@ import me.minidigger.hangar.model.generated.ProjectStatsAll;
import me.minidigger.hangar.model.viewhelpers.ProjectApprovalData;
import me.minidigger.hangar.model.viewhelpers.ProjectMember;
import me.minidigger.hangar.model.viewhelpers.ScopedProjectData;
import me.minidigger.hangar.model.viewhelpers.UnhealthyProject;
import me.minidigger.hangar.service.project.ProjectFactory.InvalidProjectReason;
import org.jdbi.v3.sqlobject.config.RegisterBeanMapper;
import org.jdbi.v3.sqlobject.customizer.BindBean;
import org.jdbi.v3.sqlobject.customizer.Define;
import org.jdbi.v3.sqlobject.customizer.Timestamped;
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import org.jdbi.v3.stringtemplate4.UseStringTemplateEngine;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.util.List;
import java.util.Map;
@ -140,4 +144,14 @@ public interface ProjectDao {
" WHERE vc.resolved_at IS NULL" +
" AND p.visibility = 2")
List<ProjectApprovalData> getVisibilityWaitingProject();
@RegisterBeanMapper(UnhealthyProject.class)
@UseStringTemplateEngine
@SqlQuery("SELECT p.owner_name, p.slug, p.topic_id, p.post_id, coalesce(hp.last_updated, p.created_at), p.visibility" +
" FROM projects p JOIN home_projects hp ON p.id = hp.id" +
" WHERE p.topic_id IS NULL" +
" OR p.post_id IS NULL" +
" OR hp.last_updated > (now() - make_interval(secs := <age>))" +
" OR p.visibility != 0")
List<UnhealthyProject> getUnhealthyProjects(@Define("age") long staleAgeSeconds);
}

View File

@ -0,0 +1,59 @@
package me.minidigger.hangar.model.viewhelpers;
import me.minidigger.hangar.model.Visibility;
import me.minidigger.hangar.model.generated.ProjectNamespace;
import org.jdbi.v3.core.mapper.Nested;
import java.time.OffsetDateTime;
public class UnhealthyProject {
private ProjectNamespace namespace;
private Integer topicId;
private Integer postId;
private OffsetDateTime lastUpdated;
private Visibility visibility;
public UnhealthyProject() { }
public ProjectNamespace getNamespace() {
return namespace;
}
@Nested("pn")
public void setNamespace(ProjectNamespace namespace) {
this.namespace = namespace;
}
public Integer getTopicId() {
return topicId;
}
public void setTopicId(Integer topicId) {
this.topicId = topicId;
}
public Integer getPostId() {
return postId;
}
public void setPostId(Integer postId) {
this.postId = postId;
}
public OffsetDateTime getLastUpdated() {
return lastUpdated;
}
public void setLastUpdated(OffsetDateTime lastUpdated) {
this.lastUpdated = lastUpdated;
}
public Visibility getVisibility() {
return visibility;
}
public void setVisibility(Visibility visibility) {
this.visibility = visibility;
}
}

View File

@ -0,0 +1,24 @@
package me.minidigger.hangar.service;
import me.minidigger.hangar.db.dao.HangarDao;
import me.minidigger.hangar.db.dao.JobDao;
import me.minidigger.hangar.db.model.JobsTable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class JobService {
private final HangarDao<JobDao> jobDao;
@Autowired
public JobService(HangarDao<JobDao> jobDao) {
this.jobDao = jobDao;
}
public List<JobsTable> getErroredJobs() {
return jobDao.get().getErroredJobs();
}
}

View File

@ -1,19 +1,6 @@
package me.minidigger.hangar.service.project;
import org.postgresql.shaded.com.ongres.scram.common.util.Preconditions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import me.minidigger.hangar.config.hangar.HangarConfig;
import me.minidigger.hangar.db.dao.HangarDao;
import me.minidigger.hangar.db.dao.ProjectDao;
import me.minidigger.hangar.db.dao.UserDao;
@ -34,13 +21,28 @@ import me.minidigger.hangar.model.viewhelpers.ProjectFlag;
import me.minidigger.hangar.model.viewhelpers.ProjectMember;
import me.minidigger.hangar.model.viewhelpers.ProjectViewSettings;
import me.minidigger.hangar.model.viewhelpers.ScopedProjectData;
import me.minidigger.hangar.model.viewhelpers.UnhealthyProject;
import me.minidigger.hangar.model.viewhelpers.UserRole;
import me.minidigger.hangar.service.UserService;
import me.minidigger.hangar.util.StringUtils;
import org.postgresql.shaded.com.ongres.scram.common.util.Preconditions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class ProjectService {
private final HangarConfig hangarConfig;
private final HangarDao<ProjectDao> projectDao;
private final HangarDao<UserDao> userDao;
private final HangarDao<VisibilityDao> visibilityDao;
@ -48,7 +50,8 @@ public class ProjectService {
private final FlagService flagService;
@Autowired
public ProjectService(HangarDao<ProjectDao> projectDao, HangarDao<UserDao> userDao, HangarDao<VisibilityDao> visibilityDao, UserService userService, FlagService flagService) {
public ProjectService(HangarConfig hangarConfig, HangarDao<ProjectDao> projectDao, HangarDao<UserDao> userDao, HangarDao<VisibilityDao> visibilityDao, UserService userService, FlagService flagService) {
this.hangarConfig = hangarConfig;
this.projectDao = projectDao;
this.userDao = userDao;
this.visibilityDao = visibilityDao;
@ -182,4 +185,8 @@ public class ProjectService {
public List<ProjectApprovalData> getProjectsWaitingForChanges() {
return projectDao.get().getVisibilityWaitingProject();
}
public List<UnhealthyProject> getUnhealthyProjects() {
return projectDao.get().getUnhealthyProjects(hangarConfig.projects.getStaleAge().toMillis());
}
}

View File

@ -30,14 +30,14 @@
<h4 class="panel-title"><@spring.message "admin.health.discuss" /></h4>
</div>
<div class="panel-body list-group list-group-health">
@noTopicProjects.map { project =>
<div class="list-group-item">
<a class="pull-left" href="${routes.getRouteUrl("projects.show", project.namespace.ownerName, project.namespace.slug)}">
<strong>${project.namespace}</strong>
</a>
<div class="clearfix"></div>
</div>
}
<#list noTopicProjects as project>
<div class="list-group-item">
<a class="pull-left" href="${routes.getRouteUrl("projects.show", project.getNamespace().getOwner(), project.getNamespace().getSlug())}">
<strong>${project.namespace}</strong>
</a>
<div class="clearfix"></div>
</div>
</#list>
</div>
</div>
</div>
@ -47,12 +47,12 @@
<h4 class="panel-title"><@spring.message "admin.health.jobs" /></h4>
</div>
<div class="panel-body">
@erroredJobs.map { job =>
<div class="list-group-item">
Jobtype: ${job.info.jobType} Error type: ${job.info.lastErrorDescriptor} Happened: ${job.info.lastUpdated}
<div class="clearfix"></div>
</div>
}
<#list erroredJobs as job>
<div class="list-group-item">
Job type: ${job.jobType} Error type: ${job.lastErrorDescriptor} Happened: ${(job.lastUpdated).format("YYYY-mm-dd HH:mm:ss")}
<div class="clearfix"></div>
</div>
</#list>
</div>
</div>
</div>
@ -64,13 +64,13 @@
<h4 class="panel-title"><@spring.message "admin.health.stale" /></h4>
</div>
<div class="panel-body list-group list-group-health">
@staleProjects.map { project =>
<div class="list-group-item">
<a href="${routes.getRouteUrl("projects.show", project.namespace.ownerName, project.namespace.slug)}">
<strong>${project.namespace}</strong>
</a>
</div>
}
<#list staleProjects as project>
<div class="list-group-item">
<a href="${routes.getRouteUrl("projects.show", project.getNamespace().getOwner(), project.getNamespace().getSlug())}">
<strong>${project.namespace}</strong>
</a>
</div>
</#list>
</div>
</div>
</div>
@ -80,13 +80,13 @@
<h4 class="panel-title"><@spring.message "admin.health.hidden" /></h4>
</div>
<div class="panel-body list-group list-group-health">
@notPublicProjects.map { project =>
<div class="list-group-item">
<a href="${routes.getRouteUrl("projects.show", project.namespace.ownerName, project.namespace.slug)}">
<strong>${project.namespace}</strong> <small><@spring.message "visibility.name." + project.visibility.nameKey /></small>
</a>
</div>
}
<#list notPublicProjects as project>
<div class="list-group-item">
<a href="${routes.getRouteUrl("projects.show", project.getNamespace().getOwner(), project.getNamespace().getSlug())}">
<strong>${project.namespace}</strong> <small><@spring.message "visibility.name." + project.visibility.name /></small>
</a>
</div>
</#list>
</div>
</div>
</div>
@ -108,13 +108,13 @@
<h4 class="panel-title"><@spring.message "admin.health.missingFile" /></h4>
</div>
<div class="panel-body list-group list-group-health">
@missingFileProjects.map { case (version, project) =>
<#--@missingFileProjects.map { case (version, project) =>
<div class="list-group-item">
<a href="${routes.getRouteUrl("versions.show", project.ownerName, project.slug, version.name)}">
<strong>${project.namespace}/${version.name}</strong>
</a>
</div>
}
}--> <#--TODO missinFileProjects -->
</div>
</div>
</div>