User Agent Stats

pull/8/head
Qstick 3 years ago
parent 2f702ba06d
commit 0b45929ccc

@ -1,6 +1,7 @@
import Chart from 'chart.js'; import Chart from 'chart.js';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import colors from 'Styles/Variables/colors';
class BarChart extends Component { class BarChart extends Component {
constructor(props) { constructor(props) {
@ -10,7 +11,7 @@ class BarChart extends Component {
componentDidMount() { componentDidMount() {
this.myChart = new Chart(this.canvasRef.current, { this.myChart = new Chart(this.canvasRef.current, {
type: 'bar', type: this.props.horizontal ? 'horizontalBar' : 'bar',
options: { options: {
maintainAspectRatio: false maintainAspectRatio: false
}, },
@ -18,7 +19,8 @@ class BarChart extends Component {
labels: this.props.data.map((d) => d.label), labels: this.props.data.map((d) => d.label),
datasets: [{ datasets: [{
label: this.props.title, label: this.props.title,
data: this.props.data.map((d) => d.value) data: this.props.data.map((d) => d.value),
backgroundColor: colors.chartColors
}] }]
} }
}); });
@ -39,11 +41,13 @@ class BarChart extends Component {
BarChart.propTypes = { BarChart.propTypes = {
data: PropTypes.arrayOf(PropTypes.object).isRequired, data: PropTypes.arrayOf(PropTypes.object).isRequired,
horizontal: PropTypes.bool,
title: PropTypes.string.isRequired title: PropTypes.string.isRequired
}; };
BarChart.defaultProps = { BarChart.defaultProps = {
data: [], data: [],
horizontal: false,
title: '' title: ''
}; };

@ -1,6 +1,7 @@
import Chart from 'chart.js'; import Chart from 'chart.js';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import colors from 'Styles/Variables/colors';
class DoughnutChart extends Component { class DoughnutChart extends Component {
constructor(props) { constructor(props) {
@ -25,7 +26,8 @@ class DoughnutChart extends Component {
labels: this.props.data.map((d) => d.label), labels: this.props.data.map((d) => d.label),
datasets: [{ datasets: [{
label: this.props.title, label: this.props.title,
data: this.props.data.map((d) => d.value) data: this.props.data.map((d) => d.value),
backgroundColor: colors.chartColors
}] }]
} }
}); });

@ -4,6 +4,7 @@ import BarChart from 'Components/Chart/BarChart';
import DoughnutChart from 'Components/Chart/DoughnutChart'; import DoughnutChart from 'Components/Chart/DoughnutChart';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import getErrorMessage from 'Utilities/Object/getErrorMessage'; import getErrorMessage from 'Utilities/Object/getErrorMessage';
import styles from './Stats.css'; import styles from './Stats.css';
@ -45,60 +46,106 @@ function getNumberGrabsData(indexerStats) {
return data; return data;
} }
function getUserAgentGrabsData(indexerStats) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.userAgent ? indexer.userAgent : 'Other',
value: indexer.numberOfGrabs
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getUserAgentQueryData(indexerStats) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.userAgent ? indexer.userAgent : 'Other',
value: indexer.numberOfQueries
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function Stats(props) { function Stats(props) {
const { const {
items, item,
isFetching, isFetching,
isPopulated, isPopulated,
error error
} = props; } = props;
const isLoaded = !!(!error && isPopulated && items.length); const isLoaded = !!(!error && isPopulated);
return ( return (
<PageContent> <PageContent>
<PageToolbar /> <PageToolbar />
{ <PageContentBody>
isFetching && !isPopulated && {
<LoadingIndicator /> isFetching && !isPopulated &&
} <LoadingIndicator />
}
{
!isFetching && !!error && {
<div className={styles.errorMessage}> !isFetching && !!error &&
{getErrorMessage(error, 'Failed to load indexer stats from API')} <div className={styles.errorMessage}>
</div> {getErrorMessage(error, 'Failed to load indexer stats from API')}
}
{
isLoaded &&
<div>
<div className={styles.fullWidthChart}>
<BarChart
data={getAverageResponseTimeData(items)}
title='Average Response Times (ms)'
/>
</div>
<div className={styles.halfWidthChart}>
<DoughnutChart
data={getTotalRequestsData(items)}
title='Total Indexer Queries'
/>
</div> </div>
<div className={styles.halfWidthChart}> }
<BarChart
data={getNumberGrabsData(items)} {
title='Total Indexer Grabs' isLoaded &&
/> <div>
<div className={styles.fullWidthChart}>
<BarChart
data={getAverageResponseTimeData(item.indexers)}
title='Average Response Times (ms)'
/>
</div>
<div className={styles.halfWidthChart}>
<DoughnutChart
data={getTotalRequestsData(item.indexers)}
title='Total Indexer Queries'
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getNumberGrabsData(item.indexers)}
title='Total Indexer Grabs'
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getUserAgentQueryData(item.userAgents)}
title='Total User Agent Queries'
horizontal={true}
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getUserAgentGrabsData(item.userAgents)}
title='Total User Agent Grabs'
horizontal={true}
/>
</div>
</div> </div>
</div> }
} </PageContentBody>
</PageContent> </PageContent>
); );
} }
Stats.propTypes = { Stats.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired, item: PropTypes.object.isRequired,
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,

@ -14,13 +14,13 @@ export const defaultState = {
isFetching: false, isFetching: false,
isPopulated: false, isPopulated: false,
error: null, error: null,
items: [], item: {},
details: { details: {
isFetching: false, isFetching: false,
isPopulated: false, isPopulated: false,
error: null, error: null,
items: [] item: []
} }
}; };

@ -181,5 +181,10 @@ module.exports = {
// //
// Table // Table
tableRowHoverBackgroundColor: '#fafbfc' tableRowHoverBackgroundColor: '#fafbfc',
//
// Charts
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26']
}; };

@ -10,4 +10,18 @@ namespace NzbDrone.Core.IndexerStats
public int NumberOfQueries { get; set; } public int NumberOfQueries { get; set; }
public int NumberOfGrabs { get; set; } public int NumberOfGrabs { get; set; }
} }
public class UserAgentStatistics : ResultSet
{
public string UserAgent { get; set; }
public int NumberOfQueries { get; set; }
public int NumberOfGrabs { get; set; }
}
public class HostStatistics : ResultSet
{
public string Host { get; set; }
public int NumberOfQueries { get; set; }
public int NumberOfGrabs { get; set; }
}
} }

@ -10,6 +10,7 @@ namespace NzbDrone.Core.IndexerStats
public interface IIndexerStatisticsRepository public interface IIndexerStatisticsRepository
{ {
List<IndexerStatistics> IndexerStatistics(); List<IndexerStatistics> IndexerStatistics();
List<UserAgentStatistics> UserAgentStatistics();
} }
public class IndexerStatisticsRepository : IIndexerStatisticsRepository public class IndexerStatisticsRepository : IIndexerStatisticsRepository
@ -26,13 +27,13 @@ namespace NzbDrone.Core.IndexerStats
public List<IndexerStatistics> IndexerStatistics() public List<IndexerStatistics> IndexerStatistics()
{ {
var time = DateTime.UtcNow; var time = DateTime.UtcNow;
return Query(Builder()); return Query(IndexerBuilder());
} }
public List<IndexerStatistics> IndexerStatistics(int indexerId) public List<UserAgentStatistics> UserAgentStatistics()
{ {
var time = DateTime.UtcNow; var time = DateTime.UtcNow;
return Query(Builder().Where<IndexerDefinition>(x => x.Id == indexerId)); return UserAgentQuery(UserAgentBuilder());
} }
private List<IndexerStatistics> Query(SqlBuilder builder) private List<IndexerStatistics> Query(SqlBuilder builder)
@ -45,7 +46,17 @@ namespace NzbDrone.Core.IndexerStats
} }
} }
private SqlBuilder Builder() => new SqlBuilder() private List<UserAgentStatistics> UserAgentQuery(SqlBuilder builder)
{
var sql = builder.AddTemplate(_selectTemplate).LogQuery();
using (var conn = _database.OpenConnection())
{
return conn.Query<UserAgentStatistics>(sql.RawSql, sql.Parameters).ToList();
}
}
private SqlBuilder IndexerBuilder() => new SqlBuilder()
.Select(@"Indexers.Id AS IndexerId, .Select(@"Indexers.Id AS IndexerId,
Indexers.Name AS IndexerName, Indexers.Name AS IndexerName,
SUM(CASE WHEN EventType == 2 then 1 else 0 end) AS NumberOfQueries, SUM(CASE WHEN EventType == 2 then 1 else 0 end) AS NumberOfQueries,
@ -53,5 +64,11 @@ namespace NzbDrone.Core.IndexerStats
AVG(json_extract(History.Data,'$.elapsedTime')) AS AverageResponseTime") AVG(json_extract(History.Data,'$.elapsedTime')) AS AverageResponseTime")
.Join<History.History, IndexerDefinition>((t, r) => t.IndexerId == r.Id) .Join<History.History, IndexerDefinition>((t, r) => t.IndexerId == r.Id)
.GroupBy<IndexerDefinition>(x => x.Id); .GroupBy<IndexerDefinition>(x => x.Id);
private SqlBuilder UserAgentBuilder() => new SqlBuilder()
.Select(@"json_extract(History.Data,'$.source') AS UserAgent,
SUM(CASE WHEN EventType == 2 then 1 else 0 end) AS NumberOfQueries,
SUM(CASE WHEN EventType == 1 then 1 else 0 end) AS NumberOfGrabs")
.GroupBy("UserAgent");
} }
} }

@ -6,6 +6,7 @@ namespace NzbDrone.Core.IndexerStats
public interface IIndexerStatisticsService public interface IIndexerStatisticsService
{ {
List<IndexerStatistics> IndexerStatistics(); List<IndexerStatistics> IndexerStatistics();
List<UserAgentStatistics> UserAgentStatistics();
} }
public class IndexerStatisticsService : IIndexerStatisticsService public class IndexerStatisticsService : IIndexerStatisticsService
@ -23,5 +24,12 @@ namespace NzbDrone.Core.IndexerStats
return seasonStatistics.ToList(); return seasonStatistics.ToList();
} }
public List<UserAgentStatistics> UserAgentStatistics()
{
var seasonStatistics = _indexerStatisticsRepository.UserAgentStatistics();
return seasonStatistics.ToList();
}
} }
} }

@ -16,12 +16,21 @@ namespace Prowlarr.Api.V1.Indexers
{ {
_indexerStatisticsService = indexerStatisticsService; _indexerStatisticsService = indexerStatisticsService;
GetResourceAll = GetAll; Get("/", x =>
{
return GetAll();
});
} }
private List<IndexerStatsResource> GetAll() private IndexerStatsResource GetAll()
{ {
return _indexerStatisticsService.IndexerStatistics().ToResource(); var indexerResource = new IndexerStatsResource
{
Indexers = _indexerStatisticsService.IndexerStatistics(),
UserAgents = _indexerStatisticsService.UserAgentStatistics(),
};
return indexerResource;
} }
} }
} }

@ -7,35 +7,7 @@ namespace Prowlarr.Api.V1.Indexers
{ {
public class IndexerStatsResource : RestResource public class IndexerStatsResource : RestResource
{ {
public int IndexerId { get; set; } public List<IndexerStatistics> Indexers { get; set; }
public string IndexerName { get; set; } public List<UserAgentStatistics> UserAgents { get; set; }
public int NumberOfQueries { get; set; }
public int AverageResponseTime { get; set; }
public int NumberOfGrabs { get; set; }
}
public static class IndexerStatsResourceMapper
{
public static IndexerStatsResource ToResource(this IndexerStatistics model)
{
if (model == null)
{
return null;
}
return new IndexerStatsResource
{
IndexerId = model.IndexerId,
IndexerName = model.IndexerName,
NumberOfQueries = model.NumberOfQueries,
AverageResponseTime = model.AverageResponseTime,
NumberOfGrabs = model.NumberOfGrabs
};
}
public static List<IndexerStatsResource> ToResource(this IEnumerable<IndexerStatistics> models)
{
return models.Select(ToResource).ToList();
}
} }
} }

Loading…
Cancel
Save