Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions client/dashboard/app/queries/site-logs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { queryOptions, keepPreviousData } from '@tanstack/react-query';
import { fetchSiteLogs, SiteLogsParams, SiteLogsQueryOptions } from '../../data/site-logs';

export const siteLogsQuery = (
siteId: number,
params: SiteLogsParams,
options?: SiteLogsQueryOptions
) =>
queryOptions( {
queryKey: [ 'site', siteId, 'logs', params ],
queryFn: () => fetchSiteLogs( siteId, params ),
placeholderData: options?.keepPreviousData ? keepPreviousData : undefined,
enabled: params.start <= params.end,
staleTime: Infinity, // The logs within a specified time range never change.
} );
5 changes: 3 additions & 2 deletions client/dashboard/app/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@tanstack/react-router';
import { HostingFeatures } from '../data/constants';
import { fetchTwoStep } from '../data/me';
import { LogType } from '../data/site-logs';
import { canViewHundredYearPlanSettings, canViewWordPressSettings } from '../sites/features';
import { hasHostingFeature } from '../utils/site-features';
import { hasSiteTrialEnded } from '../utils/site-trial';
Expand Down Expand Up @@ -246,7 +247,7 @@ const siteLogsPhpRoute = createRoute( {
} ).lazy( () =>
import( '../sites/logs' ).then( ( d ) =>
createLazyRoute( 'site-logs-php' )( {
component: ( props ) => <d.default { ...props } key="php" />,
component: () => <d.default logType={ LogType.PHP } />,
} )
)
);
Expand All @@ -257,7 +258,7 @@ const siteLogsServerRoute = createRoute( {
} ).lazy( () =>
import( '../sites/logs' ).then( ( d ) =>
createLazyRoute( 'site-logs-server' )( {
component: ( props ) => <d.default { ...props } key="server" />,
component: () => <d.default logType={ LogType.SERVER } />,
} )
)
);
Expand Down
137 changes: 137 additions & 0 deletions client/dashboard/data/site-logs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import wpcom from 'calypso/lib/wp';

interface SiteLogsAPIResponse {
message: string;
data: {
total_results: number | { value: number; relation: string };
logs: ( PHPLogFromEndpoint | ServerLogFromEndpoint )[];
};
}

interface PHPLogFromEndpoint {
timestamp: string;
severity: 'User' | 'Warning' | 'Deprecated' | 'Fatal error';
message: string;
kind: string;
name: string;
file: string;
line: number;
atomic_site_id: number;
}

export interface PHPLog extends Omit< PHPLogFromEndpoint, 'atomic_site_id' > {
id: string;
}

export type SiteLogsQueryOptions = { keepPreviousData?: boolean };

export const LogType = {
PHP: 'php',
SERVER: 'server',
} as const;

export type LogType = ( typeof LogType )[ keyof typeof LogType ];

export interface FilterType {
[ key: string ]: Array< string >;
}

export interface ServerLogFromEndpoint {
date: string;
request_type: 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE';
status: '200' | '301' | '302' | '400' | '401' | '403' | '404' | '429' | '500';
request_url: string;
body_bytes_sent: number;
cached: string;
http_host: string;
http_referer: string;
http2: string;
http_user_agent: string;
http_version: string;
http_x_forwarded_for: string;
renderer: string;
request_completion: string;
request_time: string;
scheme: string;
timestamp: number;
type: string;
user_ip: string;
}

export interface ServerLog extends ServerLogFromEndpoint {
id: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why this id is for... can we just utilize the timestamp field for that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When mapping in siteLogsQuery the returned data now matches the ServerLog / PHPLog types due to the added id (same as what is was before).
It seems like the timestamp itself wouldn't necessarily be unique 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had to update this now anyway with further changes -- I started by using the timestamp and another identifier and it still wasn't unique enough (though likely also thanks to the current testing environment where my dummy field data has a fixed timestamp), so there are several identifiers which help ensure the data has a unique id.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see... we ideally should add this in the backend side, but we can do it later. 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point I added a task for it at DOTCOM-14208

}

export interface SiteLogsParams {
logType: LogType;
start: number;
end: number;
filter: FilterType;
sortOrder?: 'asc' | 'desc';
pageSize?: number;
pageIndex?: number;
}

export interface SiteLogsData {
total_results: number;
logs: ( PHPLog | ServerLog )[];
}

export async function fetchSiteLogs(
siteId: number,
{ logType, start, end, filter, sortOrder, pageSize, pageIndex }: SiteLogsParams
): Promise< SiteLogsData > {
const logTypeFragment = logType === LogType.PHP ? 'error-logs' : 'logs';
const path = `/sites/${ siteId }/hosting/${ logTypeFragment }`;

const queryParams = {
start,
end,
filter,
sort_order: sortOrder,
page_size: pageSize,
page_index: pageIndex,
};

// Remove undefined values from queryParams
Object.keys( queryParams ).forEach(
( key ) =>
( queryParams as Record< string, unknown > )[ key ] === undefined &&
delete ( queryParams as Record< string, unknown > )[ key ]
);

const response = await wpcom.req.get(
{
path,
apiNamespace: 'wpcom/v2',
},
{ ...queryParams }
);

const { data } = response as SiteLogsAPIResponse;

const totalResults =
typeof data.total_results === 'number' ? data.total_results : data.total_results?.value ?? 0;

const logs = Array.isArray( data.logs ) ? data.logs : [];

if ( logType === LogType.PHP ) {
const normalized = ( logs as PHPLogFromEndpoint[] ).map(
( { atomic_site_id, ...logWithoutAtomicId }, index ) => ( {
...logWithoutAtomicId,
id: `${ logWithoutAtomicId.timestamp }|${ logWithoutAtomicId.file }|${ String(
logWithoutAtomicId.line
) }|${ String( pageIndex ?? 0 ) }|${ String( index ) }`,
} )
);
return { total_results: totalResults, logs: normalized };
}

const normalized = ( logs as ServerLogFromEndpoint[] ).map( ( log, index ) => ( {
...log,
id: `${ String( log.timestamp ) }|${ log.request_type }|${ log.status }|${ log.request_url }|${
log.user_ip
}|${ String( pageIndex ?? 0 ) }|${ String( index ) }`,
} ) );
return { total_results: totalResults, logs: normalized };
}
21 changes: 21 additions & 0 deletions client/dashboard/sites/logs/dataviews/views.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { LogType, FilterType } from '../../../data/site-logs';
import type { View } from '@wordpress/dataviews';

const getFilterParamsFromView = ( view: View, fieldNames: string[] ): FilterType => {
return ( view.filters || [] )
.filter( ( filter ) => fieldNames.includes( filter.field ) )
.reduce( ( acc: FilterType, filter ) => {
if ( filter.value ) {
acc[ filter.field ] = filter.value;
}
return acc;
}, {} as FilterType );
};

export function toFilterParams( { view, logType }: { view: View; logType: LogType } ): FilterType {
if ( logType === LogType.PHP ) {
return getFilterParamsFromView( view, [ 'severity' ] );
}

return getFilterParamsFromView( view, [ 'cached', 'request_type', 'status', 'renderer' ] );
}
Loading