Skip to content

Commit

Permalink
UI: Show agents on Targets page (#1934)
Browse files Browse the repository at this point in the history
* pkg/profilestore: Identify agents by name and ip

This is so that agents running on the same node but with different name will be tracked separately too.

* ui: Create AgentsTable to show on the targets page

This uses the new AgentService that tracks the agents writing into Parca.
  • Loading branch information
metalmatze authored Oct 20, 2022
1 parent ac98e12 commit a60fc59
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 24 deletions.
10 changes: 5 additions & 5 deletions pkg/profilestore/profilecolumnstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,11 @@ func (s *ProfileColumnStore) writeSeries(ctx context.Context, req *profilestorep
return nil
}

func (s *ProfileColumnStore) updateAgents(ip string, ag agent) {
func (s *ProfileColumnStore) updateAgents(nodeNameAndIP string, ag agent) {
s.mtx.Lock()
defer s.mtx.Unlock()

s.agents[ip] = ag
s.agents[nodeNameAndIP] = ag

for i, a := range s.agents {
if a.lastPush.Before(time.Now().Add(-5 * time.Minute)) {
Expand Down Expand Up @@ -222,7 +222,7 @@ func (s *ProfileColumnStore) WriteRaw(ctx context.Context, req *profilestorepb.W
ipPort := p.Addr.String()
ip := ipPort[:strings.LastIndex(ipPort, ":")]

s.updateAgents(ip, ag)
s.updateAgents(nodeName+ip, ag)
}

if writeErr != nil {
Expand All @@ -237,7 +237,7 @@ func (s *ProfileColumnStore) Agents(ctx context.Context, req *profilestorepb.Age
defer s.mtx.Unlock()

agents := make([]*profilestorepb.Agent, 0, len(s.agents))
for ip, ag := range s.agents {
for nodeNameAndIP, ag := range s.agents {
lastError := ""
lerr := ag.lastError
if lerr != nil {
Expand All @@ -246,7 +246,7 @@ func (s *ProfileColumnStore) Agents(ctx context.Context, req *profilestorepb.Age

id := ag.nodeName
if id == "" {
id = ip
id = nodeNameAndIP
}

agents = append(agents, &profilestorepb.Agent{
Expand Down
115 changes: 115 additions & 0 deletions ui/packages/app/web/src/components/Targets/AgentsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright 2022 The Parca Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import React from 'react';
import {Agent} from '@parca/client';
import LastScrapeCell from './LastScrapeCell';
import {TimeObject} from '@parca/functions';

enum AgentsTableHeader {
id = 'Name',
lastPush = 'Last Push',
lastError = 'Last Error',
}

const getRowContentByHeader = ({
header,
agent,
key,
}: {
header: string;
agent: Agent;
key: string;
}) => {
switch (header) {
case AgentsTableHeader.id: {
return (
<td key={key} className="px-6 py-4 whitespace-nowrap">
{agent.id}
</td>
);
}
case AgentsTableHeader.lastError: {
return (
<td
key={key}
className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-200"
>
{agent.lastError}
</td>
);
}
case AgentsTableHeader.lastPush: {
const lastPush: TimeObject =
agent.lastPush !== undefined
? {
// Warning: string to number can overflow
// https://github.com/timostamm/protobuf-ts/blob/master/MANUAL.md#bigint-support
seconds: Number(agent.lastPush.seconds),
nanos: agent.lastPush.nanos,
}
: {};
const lastPushDuration: TimeObject =
agent.lastPushDuration !== undefined
? {
// Warning: string to number can overflow
// https://github.com/timostamm/protobuf-ts/blob/master/MANUAL.md#bigint-support
seconds: Number(agent.lastPushDuration.seconds),
nanos: agent.lastPushDuration.nanos,
}
: {};
return (
<LastScrapeCell key={key} lastScrape={lastPush} lastScrapeDuration={lastPushDuration} />
);
}
default: {
return <td />;
}
}
};

const AgentsTable = ({agents}: {agents: Agent[]}) => {
const headers = Object.keys(AgentsTableHeader) as (keyof typeof AgentsTableHeader)[];

return (
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
{headers.map(header => (
<th
key={header}
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-200 uppercase tracking-wider"
>
{AgentsTableHeader[header]}
</th>
))}
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200 dark:bg-gray-900 dark:divide-gray-700">
{agents.map((agent: Agent) => {
return (
<tr key={agent.id}>
{headers.map(header => {
const key = `table-cell-${header}-${agent.id}`;
return getRowContentByHeader({header: AgentsTableHeader[header], agent, key});
})}
</tr>
);
})}
</tbody>
</table>
);
};

export default AgentsTable;
86 changes: 67 additions & 19 deletions ui/packages/app/web/src/pages/targets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,20 @@

import React, {useEffect, useState} from 'react';
import {
Agent,
AgentsResponse,
AgentsServiceClient,
ScrapeServiceClient,
Target,
Targets,
TargetsResponse,
TargetsRequest_State,
TargetsResponse,
} from '@parca/client';
import {RpcError} from '@protobuf-ts/runtime-rpc';
import {EmptyState} from '@parca/components';
import TargetsTable from '../components/Targets/TargetsTable';
import {GrpcWebFetchTransport} from '@protobuf-ts/grpcweb-transport';
import AgentsTable from '../components/Targets/AgentsTable';

const apiEndpoint = process.env.REACT_APP_PUBLIC_API_ENDPOINT;

Expand Down Expand Up @@ -61,10 +65,42 @@ const sortTargets = (targets: {[x: string]: any}[]) =>
return Object.keys(a)[0].localeCompare(Object.keys(b)[0]);
});

export interface IAgentsResult {
response: AgentsResponse | null;
error: RpcError | null;
}
export const useAgents = (client: AgentsServiceClient): IAgentsResult => {
const [result, setResult] = useState<IAgentsResult>({
response: null,
error: null,
});

useEffect(() => {
const call = client.agents({});

call.response
.then(response => setResult({response, error: null}))
.catch(error => setResult({error, response: null}));
}, [client]);

return result;
};

const agentsClient = new AgentsServiceClient(
new GrpcWebFetchTransport({
baseUrl: apiEndpoint === undefined ? `${window.PATH_PREFIX}/api` : `${apiEndpoint}/api`,
})
);

const sortAgents = (agents: Agent[]) =>
agents.sort((a: Agent, b: Agent) => a.id.localeCompare(b.id));

const TargetsPage = (): JSX.Element => {
const {response: targetsResponse, error} = useTargets(scrapeClient);
const {response: targetsResponse, error: targetsError} = useTargets(scrapeClient);
const {response: agentsResponse, error: agentsError} = useAgents(agentsClient);

if (error !== null) {
// TODO: We should support showing the other type's response if only one errors.
if (targetsError !== null || agentsError !== null) {
return <div>Error</div>;
}

Expand All @@ -77,12 +113,14 @@ const TargetsPage = (): JSX.Element => {
getKeyValuePairFromArray(item[0], item[1])
);

const agents = agentsResponse?.agents ?? [];

return (
<div className="flex flex-col">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<EmptyState
isEmpty={targetNamespaces?.length <= 0}
isEmpty={targetNamespaces?.length <= 0 && agents.length <= 0}
title="No targets available"
body={
<p>
Expand All @@ -97,22 +135,32 @@ const TargetsPage = (): JSX.Element => {
</p>
}
>
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
{sortTargets(targetNamespaces)?.map(namespace => {
const name = Object.keys(namespace)[0];
const targets = namespace[name].sort((a: Target, b: Target) => {
return a.url.localeCompare(b.url);
});
return (
<div key={name} className="my-2 p-2 border-b-2">
<div className="my-2">
<span className="font-semibold text-xl">{name}</span>
</div>
<TargetsTable targets={targets} />
<>
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
<div className="my-2 p-2 border-b-2">
<div className="my-2">
<span className="font-semibold text-xl">Parca Agents</span>
</div>
);
})}
</div>
<AgentsTable agents={sortAgents(agents)} />
</div>
</div>
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
{sortTargets(targetNamespaces)?.map(namespace => {
const name = Object.keys(namespace)[0];
const targets = namespace[name].sort((a: Target, b: Target) => {
return a.url.localeCompare(b.url);
});
return (
<div key={name} className="my-2 p-2 border-b-2">
<div className="my-2">
<span className="font-semibold text-xl">{name}</span>
</div>
<TargetsTable targets={targets} />
</div>
);
})}
</div>
</>
</EmptyState>
</div>
</div>
Expand Down

0 comments on commit a60fc59

Please sign in to comment.