Skip to content

Commit

Permalink
Exceptions: allow deletion of searches
Browse files Browse the repository at this point in the history
This also collapses a lot of code, but very unhappy with it - need to get into a consistent model binder instead. It's a step towards that, though. I want to re-pave this, but don't want to break existing links.
  • Loading branch information
Nick Craver committed Jun 8, 2018
1 parent 94b7f32 commit bd8cb84
Show file tree
Hide file tree
Showing 11 changed files with 131 additions and 274 deletions.
72 changes: 42 additions & 30 deletions Opserver.Core/Data/Exceptions/ExceptionStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ public class SearchParams
public DateTime? EndDate { get; set; }
public Guid? StartAt { get; set; }
public ExceptionSorts Sort { get; set; }
public Guid? Id { get; set; }

public override int GetHashCode()
{
Expand All @@ -183,12 +184,21 @@ public override int GetHashCode()
hashCode = (hashCode * -1521134295) + EqualityComparer<DateTime?>.Default.GetHashCode(StartDate);
hashCode = (hashCode * -1521134295) + EqualityComparer<DateTime?>.Default.GetHashCode(EndDate);
hashCode = (hashCode * -1521134295) + EqualityComparer<Guid?>.Default.GetHashCode(StartAt);
hashCode = (hashCode * -1521134295) + EqualityComparer<Guid?>.Default.GetHashCode(Id);
return (hashCode * -1521134295) + Sort.GetHashCode();
}
}

// TODO: Move this into a SQL-specific provider maybe, something swappable
public Task<List<Error>> GetErrorsAsync(SearchParams search)
{
var query = GetSearchQuery(search, QueryMode.Search);
return QueryListAsync<Error>($"{nameof(GetErrorsAsync)}() for {Name}", query.SQL, query.Params);
}

private enum QueryMode { Search, Delete }

// TODO: Move this into a SQL-specific provider maybe, something swappable
private (string SQL, object Params) GetSearchQuery(SearchParams search, QueryMode mode)
{
var sb = StringBuilderCache.Get();
bool firstWhere = true;
Expand Down Expand Up @@ -244,28 +254,45 @@ void AddClause(string clause)
{
AddClause("(Message Like @query Or Url Like @query)");
}
sb.Append(@")
Select Top {=Count} *
From list");
if (search.StartAt.HasValue)
if (search.Id.HasValue)
{
sb.Append(@"
Where rowNum > (Select Top 1 rowNum From list Where GUID = @StartAt)");
AddClause("Id = @Id");
}
if (mode == QueryMode.Delete)
{
AddClause("IsProtected = 0");
}
sb.Append(@"
Order By rowNum");

var sql = sb.ToStringRecycle();
sb.AppendLine(")");
switch (mode)
{
case QueryMode.Search:
sb.AppendLine(" Select Top {=Count} *");
break;
case QueryMode.Delete:
sb.AppendLine(" Delete");
break;
}
sb.AppendLine(" From list");
if (search.StartAt.HasValue)
{
sb.AppendLine(" Where rowNum > (Select Top 1 rowNum From list Where GUID = @StartAt)");
}
if (mode == QueryMode.Search)
{
sb.AppendLine("Order By rowNum");
}

return QueryListAsync<Error>($"{nameof(GetErrorsAsync)}() for {Name}", sql, new
return (sb.ToStringRecycle(), new
{
logs,
search.Message,
search.StartDate,
search.EndDate,
query = "%" + search.SearchQuery + "%",
search.StartAt,
search.Count
search.Count,
search.Id
});
}

Expand Down Expand Up @@ -334,25 +361,10 @@ private string GetSortString(ExceptionSorts sort)
}
}

public Task<int> DeleteAllErrorsAsync(List<string> apps)
{
return ExecTaskAsync($"{nameof(DeleteAllErrorsAsync)}() for {Name}", @"
Update Exceptions
Set DeletionDate = GETUTCDATE()
Where DeletionDate Is Null
And IsProtected = 0
And ApplicationName In @apps", new { apps });
}

public Task<int> DeleteSimilarErrorsAsync(Error error)
public Task<int> DeleteErrorsAsync(SearchParams search)
{
return ExecTaskAsync($"{nameof(DeleteSimilarErrorsAsync)}('{error.GUID}') (app: {error.ApplicationName}) for {Name}", @"
Update Exceptions
Set DeletionDate = GETUTCDATE()
Where ApplicationName = @ApplicationName
And Message = @Message
And DeletionDate Is Null
And IsProtected = 0", new {error.ApplicationName, error.Message});
var query = GetSearchQuery(search, QueryMode.Delete);
return ExecTaskAsync($"{nameof(DeleteErrorsAsync)}() for {Name}", query.SQL, query.Params);
}

public Task<int> DeleteErrorsAsync(List<Guid> ids)
Expand Down
33 changes: 0 additions & 33 deletions Opserver/Content/js/Scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -1288,11 +1288,6 @@ Status.Exceptions = (function () {
jThis.find('.fa').addClass('icon-rotate-flip');
$.ajax({
type: 'POST',
data: $.extend({}, baseOptions, {
group: jThis.data('group') || options.group,
log: jThis.data('log') || options.log,
id: jThis.data('id') || options.id
}),
url: jThis.data('url'),
success: function (data) {
if (data.url) {
Expand All @@ -1311,34 +1306,6 @@ Status.Exceptions = (function () {
return false;
});

$(document).on('click', 'a.js-clear-visible', function () {
var jThis = $(this);
bootbox.confirm('Really delete all visible, non-protected errors?', function (result) {
if (result)
{
var ids = $('.js-error:not(.protected,.deleted)').map(function () { return $(this).data('id'); }).get();
jThis.find('.fa').addClass('icon-rotate-flip');

$.ajax({
type: 'POST',
traditional: true,
data: $.extend({}, baseOptions, {
ids: ids
}),
url: jThis.data('url'),
success: function (data) {
window.location.href = data.url;
},
error: function (xhr) {
jThis.find('.fa').removeClass('icon-rotate-flip');
jThis.parent().errorPopupFromJSON(xhr, 'An error occurred clearing visible exceptions');
}
});
}
});
return false;
});

$.tablesorter.addParser({
id: 'errorCount',
is: function () { return false; },
Expand Down
130 changes: 49 additions & 81 deletions Opserver/Controllers/ExceptionsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,17 @@ public class ExceptionsController : StatusController
private ExceptionStore CurrentStore;
private string CurrentGroup;
private string CurrentLog;
private Guid? CurrentId;
private Guid? CurrentSimilarId;
private ExceptionSorts CurrentSort;

protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
CurrentStore = ExceptionsModule.GetStore(Request.Params["store"]);
CurrentGroup = Request.Params["group"];
CurrentLog = Request.Params["log"] ?? Request.Params["app"]; // old link compat
CurrentId = Request.Params["id"].HasValue() && Guid.TryParse(Request.Params["id"], out var guid) ? guid : (Guid?)null;
CurrentSimilarId = Request.Params["similar"].HasValue() && Guid.TryParse(Request.Params["similar"], out var similarGuid) ? similarGuid : (Guid?)null;
Enum.TryParse(Request.Params["sort"], out CurrentSort);

if (CurrentLog.HasValue())
Expand Down Expand Up @@ -65,14 +69,36 @@ protected override void OnActionExecuting(ActionExecutingContext filterContext)
base.OnActionExecuting(filterContext);
}

private ExceptionStore.SearchParams GetSearch()
// TODO: Move entirely to model binder
private async Task<ExceptionStore.SearchParams> GetSearchAsync()
{
return new ExceptionStore.SearchParams
var result = new ExceptionStore.SearchParams
{
Group = CurrentGroup,
Log = CurrentLog,
Sort = CurrentSort
Sort = CurrentSort,
Id = CurrentId
};

if (Request.Params["q"].HasValue())
{
result.SearchQuery = Request.Params["q"];
}
if (bool.TryParse(Request.Params["includeDeleted"], out var includeDeleted))
{
result.IncludeDeleted = includeDeleted;
}

if (CurrentSimilarId.HasValue)
{
var error = await CurrentStore.GetErrorAsync(CurrentLog, CurrentSimilarId.Value).ConfigureAwait(false);
if (error != null)
{
result.Message = error.Message;
}
}

return result;
}

private ExceptionsModel GetModel(List<Exceptional.Error> errors)
Expand All @@ -96,82 +122,30 @@ private ExceptionsModel GetModel(List<Exceptional.Error> errors)
}

[Route("exceptions")]
public async Task<ActionResult> Exceptions(string q, bool showDeleted = false)
public async Task<ActionResult> Exceptions()
{
var search = GetSearch();
search.SearchQuery = q;
search.IncludeDeleted = showDeleted;

var search = await GetSearchAsync().ConfigureAwait(false);
var errors = await CurrentStore.GetErrorsAsync(search).ConfigureAwait(false);

var vd = GetModel(errors);
vd.Search = q;
vd.SearchParams = search;
vd.LoadAsyncSize = Current.Settings.Exceptions.PageSize;
return View(vd);
}

[Route("exceptions/load-more")]
public async Task<ActionResult> LoadMore(string q, bool showDeleted = false, int? count = null, Guid? prevLast = null)
public async Task<ActionResult> LoadMore(int? count = null, Guid? prevLast = null)
{
var search = GetSearch();
search.SearchQuery = q;
var search = await GetSearchAsync().ConfigureAwait(false);
search.Count = count ?? Current.Settings.Exceptions.PageSize;
search.StartAt = prevLast;
search.IncludeDeleted = showDeleted;

var errors = await CurrentStore.GetErrorsAsync(search).ConfigureAwait(false);
var vd = GetModel(errors);
vd.Search = q;
vd.SearchParams = search;
return View("Exceptions.Table.Rows", vd);
}

[Route("exceptions/similar")]
public async Task<ActionResult> Similar(Guid id, bool byTime = false, int rangeInSeconds = 5 * 60)
{
var e = await CurrentStore.GetErrorAsync(CurrentLog, id).ConfigureAwait(false);
if (e == null)
return View("Exceptions.Detail", null);

var search = GetSearch();
if (byTime)
{
search.StartDate = e.CreationDate.AddMinutes(-rangeInSeconds);
search.EndDate = e.CreationDate.AddMinutes(rangeInSeconds);
}
else
{
search.Message = e.Message;
}

var errors = await CurrentStore.GetErrorsAsync(search).ConfigureAwait(false);

var vd = GetModel(errors);
vd.Exception = e;
vd.ClearLinkForVisibleOnly = true;
return View("Exceptions.Similar", vd);
}

// TODO: Figure out a good "clear all" then redirect and remove this
[Route("exceptions/search")]
public async Task<ActionResult> Search(string q, bool showDeleted = false)
{
// empty searches go back to the main log
if (q.IsNullOrEmpty())
return RedirectToAction(nameof(Exceptions), new { group = CurrentGroup, log = CurrentLog });

var search = GetSearch();
search.SearchQuery = q;
search.IncludeDeleted = showDeleted;
search.Count = MaxSearchResults;

var errors = await CurrentStore.GetErrorsAsync(search).ConfigureAwait(false);
var vd = GetModel(errors);
vd.Search = q;
vd.ShowDeleted = showDeleted;
vd.ClearLinkForVisibleOnly = true;
return View("Exceptions.Search", vd);
}

[Route("exceptions/detail")]
public async Task<ActionResult> Detail(Guid id)
{
Expand Down Expand Up @@ -231,30 +205,24 @@ public async Task<ActionResult> Protect(Guid id, bool redirect = false)
}

[Route("exceptions/delete"), HttpPost, AcceptVerbs(HttpVerbs.Post), OnlyAllow(Roles.ExceptionsAdmin)]
public async Task<ActionResult> Delete(Guid id, bool redirect = false)
public async Task<ActionResult> Delete()
{
var toDelete = await GetSearchAsync().ConfigureAwait(false);
// we don't care about success...if it's *already* deleted, that's fine
// if we throw an exception trying to delete, that's another matter
await CurrentStore.DeleteErrorAsync(id).ConfigureAwait(false);

return redirect ? Json(new { url = Url.Action(nameof(Exceptions), new { store = CurrentStore.Name, group = CurrentGroup, log = CurrentLog }) }) : Counts();
}

[Route("exceptions/delete-all"), HttpPost, AcceptVerbs(HttpVerbs.Post), OnlyAllow(Roles.ExceptionsAdmin)]
public async Task<ActionResult> DeleteAll()
{
await CurrentStore.DeleteAllErrorsAsync(new List<string> { CurrentLog }).ConfigureAwait(false);

return Json(new { url = Url.Action("Exceptions", new { store = CurrentStore.Name, group = CurrentGroup }) });
}

[Route("exceptions/delete-similar"), AcceptVerbs(HttpVerbs.Post), OnlyAllow(Roles.ExceptionsAdmin)]
public async Task<ActionResult> DeleteSimilar(Guid id)
{
var e = await CurrentStore.GetErrorAsync(CurrentLog, id).ConfigureAwait(false);
await CurrentStore.DeleteSimilarErrorsAsync(e).ConfigureAwait(false);
if (toDelete.Id.HasValue)
{
// optimized single route
await CurrentStore.DeleteErrorAsync(toDelete.Id.Value).ConfigureAwait(false);
}
else
{
await CurrentStore.DeleteErrorsAsync(toDelete).ConfigureAwait(false);
}

return Json(true);
return toDelete.Id.HasValue
? Counts()
: Json(new { url = Url.Action(nameof(Exceptions), new { store = CurrentStore.Name, group = CurrentGroup, log = CurrentLog }) });
}

[Route("exceptions/delete-list"), AcceptVerbs(HttpVerbs.Post), OnlyAllow(Roles.ExceptionsAdmin)]
Expand Down
14 changes: 14 additions & 0 deletions Opserver/ExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using StackExchange.Opserver.Views.Shared;
using System.Text;
using EnumsNET;
using System.Web.Routing;

namespace StackExchange.Opserver
{
Expand Down Expand Up @@ -489,6 +490,19 @@ public static string ToQueryString(this NameValueCollection nvc)
var result = sb.ToStringRecycle();
return result.Length > 1 ? result : "";
}

public static RouteValueDictionary ToRouteValues(this NameValueCollection queryString)
{
var routeValues = new RouteValueDictionary();
if (queryString?.HasKeys() == true)
{
foreach (string key in queryString.AllKeys)
{
routeValues.Add(key, queryString[key]);
}
}
return routeValues;
}
}

public static class ViewsExtensionMethods
Expand Down
2 changes: 0 additions & 2 deletions Opserver/Opserver.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -574,8 +574,6 @@
<DependentUpon>HAProxy.cshtml</DependentUpon>
</Content>
<Content Include="Views\Shared\AccessDenied.cshtml" />
<Content Include="Views\Exceptions\Exceptions.Search.cshtml" />
<Content Include="Views\Exceptions\Exceptions.Similar.cshtml" />
<Content Include="Views\Home\About.cshtml" />
<Content Include="Views\SQL\Dashboard.cshtml" />
<Content Include="Views\SQL\Instance.DBFiles.cshtml">
Expand Down
Loading

0 comments on commit bd8cb84

Please sign in to comment.