Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MAIN-2976] new relic support #266

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,41 @@ toolsets:

</details>

<details>
<summary>New Relic Integration</summary>

The New Relic toolset (`newrelic`) allows Holmes to pull traces and logs from New Relic for analysis.

### Configuration

To enable New Relic integration, configure Holmes by adding the following settings to the configuration file:

```
nr_api_key: KEY_HERE
nr_account_id: ACCOUNT_ID_HERE
```

These parameters should be replaced with your actual New Relic API key and account ID.

### Kubernetes Deployment

To enable New Relic integration when running HolmesGPT in a Kubernetes cluster, **include the following configuration** in the `Helm chart`:

```yaml
toolsets:
newrelic:
enabled: true
config:
nr_api_key: KEY_HERE
nr_account_id: ACCOUNT_ID_HERE
```

Ensure that the API key has the necessary permissions to access traces and logs from New Relic.

For more details on New Relic's API and authentication methods, refer to [New Relic API documentation](https://docs.newrelic.com/docs/apis).

</details>
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be moved to a page on robusta docs where we gathered all the toolset documentation: https://docs.robusta.dev/master/configuration/holmesgpt/builtin_toolsets.html

Copy link
Collaborator

Choose a reason for hiding this comment

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



<details>

Expand Down
2 changes: 2 additions & 0 deletions holmes/plugins/toolsets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from holmes.plugins.toolsets.findings import FindingsToolset
from holmes.plugins.toolsets.grafana.toolset_grafana_loki import GrafanaLokiToolset
from holmes.plugins.toolsets.grafana.toolset_grafana_tempo import GrafanaTempoToolset
from holmes.plugins.toolsets.newrelic import NewRelicToolset
from holmes.plugins.toolsets.internet import InternetToolset

from holmes.core.tools import Toolset, YAMLToolset
Expand Down Expand Up @@ -46,6 +47,7 @@ def load_python_toolsets(dal: Optional[SupabaseDal]) -> List[Toolset]:
OpenSearchToolset(),
GrafanaLokiToolset(),
GrafanaTempoToolset(),
NewRelicToolset(),
]

return toolsets
Expand Down
172 changes: 172 additions & 0 deletions holmes/plugins/toolsets/newrelic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import requests
import json
import logging
from typing import Any, Optional
from holmes.core.tools import (
CallablePrerequisite,
Tool,
ToolParameter,
Toolset,
ToolsetTag,
)
from pydantic import BaseModel


class BaseNewRelicTool(Tool):
toolset: "NewRelicToolset"


class GetLogs(BaseNewRelicTool):
def __init__(self, toolset: "NewRelicToolset"):
super().__init__(
name="newrelic_get_logs",
description="Retrieve logs from New Relic",
parameters={
"app": ToolParameter(
description="The application name to filter logs",
type="string",
required=True,
),
"since": ToolParameter(
description="Time range to fetch logs (e.g., '1 hour ago')",
type="string",
required=True,
),
},
toolset=toolset,
)

def invoke(self, params: Any) -> str:
app = params.get("app")
since = params.get("since")

query = {
"query": f"""
{{
actor {{
account(id: {self.toolset.nr_account_id}) {{
nrql(query: \"SELECT * FROM Log WHERE app = '{app}' SINCE {since}\") {{
results
}}
}}
}}
}}
"""
}

url = "https://api.newrelic.com/graphql"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this be overridable for people having on premise newrelic?

headers = {
"Content-Type": "application/json",
"Api-Key": self.toolset.nr_api_key,
}

response = requests.post(url, headers=headers, json=query)
logging.info(f"Getting new relic logs for app {app} since {since}")
if response.status_code == 200:
data = response.json()
return json.dumps(data, indent=2)
else:
return f"Failed to fetch logs. Status code: {response.status_code}\n{response.text}"

def get_parameterized_one_liner(self, params) -> str:
return f"newrelic GetLogs(app='{params.get('app')}', since='{params.get('since')}')"


class GetTraces(BaseNewRelicTool):
def __init__(self, toolset: "NewRelicToolset"):
super().__init__(
name="newrelic_get_traces",
description="Retrieve traces from New Relic",
parameters={
"duration": ToolParameter(
description="Minimum trace duration in seconds",
type="number",
required=True,
),
"trace_id": ToolParameter(
description="Specific trace ID to fetch details (optional)",
type="string",
required=False,
),
},
toolset=toolset,
)

def invoke(self, params: Any) -> str:
duration = params.get("duration")
trace_id = params.get("trace_id")

if trace_id:
query_string = f"SELECT * FROM Span WHERE trace.id = '{trace_id}' and duration.ms > {duration * 1000} and span.kind != 'internal'"
else:
query_string = f"SELECT * FROM Span WHERE duration.ms > {duration * 1000} and span.kind != 'internal'"

query = {
"query": f"""
{{
actor {{
account(id: {self.toolset.nr_account_id}) {{
nrql(query: \"{query_string}\") {{
results
}}
}}
}}
}}
"""
}

url = "https://api.newrelic.com/graphql"
headers = {
"Content-Type": "application/json",
"Api-Key": self.toolset.nr_api_key,
}

response = requests.post(url, headers=headers, json=query)
logging.info(f"Getting newrelic traces longer than {duration}s")
if response.status_code == 200:
data = response.json()
return json.dumps(data, indent=2)
else:
return f"Failed to fetch traces. Status code: {response.status_code}\n{response.text}"

def get_parameterized_one_liner(self, params) -> str:
if "trace_id" in params and params["trace_id"]:
return f"newrelic GetTraces(trace_id='{params.get('trace_id')}')"
return f"newrelic GetTraces(duration={params.get('duration')})"


class NewrelicConfig(BaseModel):
nr_api_key: Optional[str] = None
nr_account_id: Optional[str] = None


class NewRelicToolset(Toolset):
nr_api_key: Optional[str] = None
nr_account_id: Optional[str] = None

def __init__(self):
super().__init__(
name="newrelic",
description="Toolset for interacting with New Relic to fetch logs and traces",
docs_url="https://docs.newrelic.com/docs/apis/nerdgraph-api/",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Similar comment to the above regarding docs, URL should be similar to https://docs.robusta.dev/master/configuration/holmesgpt/toolsets/newrelic.html

icon_url="https://upload.wikimedia.org/wikipedia/commons/4/4d/New_Relic_logo.svg",
prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
tools=[
GetLogs(self),
GetTraces(self),
],
tags=[ToolsetTag.CORE],
)

def prerequisites_callable(self, config: dict[str, Any]) -> bool:
if not config:
return False

try:
nr_config = NewrelicConfig(**config)
self.nr_account_id = nr_config.nr_account_id
self.nr_api_key = nr_config.nr_api_key
return self.nr_account_id and self.nr_api_key
except Exception:
logging.exception("Failed to set up new relic toolset")
return False
Loading