Skip to content
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
25 changes: 25 additions & 0 deletions assets/src/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3787,6 +3787,8 @@ export type DeploymentSettings = {
logging?: Maybe<LoggingSettings>;
/** the way we can connect to your loki instance */
lokiConnection?: Maybe<HttpConnection>;
/** settings for OpenTelemetry metrics export */
metrics?: Maybe<MetricsSettings>;
/** the root repo you used to run `plural up` */
mgmtRepo?: Maybe<Scalars['String']['output']>;
name: Scalars['String']['output'];
Expand Down Expand Up @@ -3821,6 +3823,8 @@ export type DeploymentSettingsAttributes = {
logging?: InputMaybe<LoggingSettingsAttributes>;
/** connection details for a loki instance to use */
lokiConnection?: InputMaybe<HttpConnectionAttributes>;
/** settings for OpenTelemetry metrics export */
metrics?: InputMaybe<MetricsSettingsAttributes>;
mgmtRepo?: InputMaybe<Scalars['String']['input']>;
/** connection details for a prometheus instance to use */
prometheusConnection?: InputMaybe<HttpConnectionAttributes>;
Expand Down Expand Up @@ -5720,6 +5724,27 @@ export type MetricResult = {
value?: Maybe<Scalars['String']['output']>;
};

/** Settings for OpenTelemetry metrics export */
export type MetricsSettings = {
__typename?: 'MetricsSettings';
/** cron expression for export schedule */
crontab?: Maybe<Scalars['String']['output']>;
/** whether metrics export is enabled */
enabled?: Maybe<Scalars['Boolean']['output']>;
/** the OpenTelemetry collector endpoint */
endpoint?: Maybe<Scalars['String']['output']>;
};

/** Settings for OpenTelemetry metrics export */
export type MetricsSettingsAttributes = {
/** cron expression for how often to export metrics (e.g. '*\/5 * * * *') */
crontab?: InputMaybe<Scalars['String']['input']>;
/** whether to enable metrics export */
enabled?: InputMaybe<Scalars['Boolean']['input']>;
/** the OpenTelemetry collector endpoint to send metrics to */
endpoint?: InputMaybe<Scalars['String']['input']>;
};

/** A monitor defines a recurring check over observability data that can raise alerts */
export type Monitor = {
__typename?: 'Monitor';
Expand Down
26 changes: 25 additions & 1 deletion go/client/models_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions go/controller/api/v1alpha1/deploymentsettings_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ type DeploymentSettingsSpec struct {
// +kubebuilder:validation:Optional
Cost *CostSettings `json:"cost,omitempty"`

// Metrics settings for OpenTelemetry metrics export
//
// +kubebuilder:validation:Optional
Metrics *MetricsSettings `json:"metrics,omitempty"`

// DeploymentRepositoryRef is a pointer to the deployment GIT repository to use
//
// +kubebuilder:validation:Optional
Expand Down Expand Up @@ -393,6 +398,38 @@ func (cost *CostSettings) Attributes() *console.CostSettingsAttributes {
}
}

// MetricsSettings holds configuration for OpenTelemetry metrics export.
type MetricsSettings struct {
// Enabled defines whether to enable the metrics export or not.
//
// +kubebuilder:default=false
// +kubebuilder:validation:Optional
Enabled *bool `json:"enabled,omitempty"`

// Endpoint is the OpenTelemetry collector endpoint to send metrics to.
//
// +kubebuilder:validation:Optional
Endpoint *string `json:"endpoint,omitempty"`

// Crontab is the cron expression for how often to export metrics.
// Example: "*/5 * * * *" for every 5 minutes.
//
// +kubebuilder:validation:Optional
Crontab *string `json:"crontab,omitempty"`
}

func (m *MetricsSettings) Attributes() *console.MetricsSettingsAttributes {
if m == nil {
return nil
}

return &console.MetricsSettingsAttributes{
Enabled: m.Enabled,
Endpoint: m.Endpoint,
Crontab: m.Crontab,
}
}

// AISettings holds the configuration for LLM provider clients.
type AISettings struct {
// Enabled defines whether to enable the AI integration or not.
Expand Down
19 changes: 19 additions & 0 deletions go/controller/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1387,6 +1387,7 @@ _Appears in:_
| `ai` _[AISettings](#aisettings)_ | AI settings specifies a configuration for LLM provider clients | | Optional: \{\} <br /> |
| `logging` _[LoggingSettings](#loggingsettings)_ | Logging settings for connections to log aggregation datastores | | Optional: \{\} <br /> |
| `cost` _[CostSettings](#costsettings)_ | Cost settings for managing Plural's cost management features | | Optional: \{\} <br /> |
| `metrics` _[MetricsSettings](#metricssettings)_ | Metrics settings for OpenTelemetry metrics export | | Optional: \{\} <br /> |
| `deploymentRepositoryRef` _[NamespacedName](#namespacedname)_ | DeploymentRepositoryRef is a pointer to the deployment GIT repository to use | | Optional: \{\} <br /> |
| `scaffoldsRepositoryRef` _[NamespacedName](#namespacedname)_ | ScaffoldsRepositoryRef is a pointer to the Scaffolds GIT repository to use | | Optional: \{\} <br /> |
| `reconciliation` _[Reconciliation](#reconciliation)_ | Reconciliation settings for this resource.<br />Controls drift detection and reconciliation intervals. | | Optional: \{\} <br /> |
Expand Down Expand Up @@ -2305,6 +2306,24 @@ _Appears in:_
| `namespace` _string_ | Namespace specifies an optional namespace for categorizing or scoping related resources.<br />If empty then the ClusterSync's namespace will be used. | | Optional: \{\} <br /> |


#### MetricsSettings



MetricsSettings holds configuration for OpenTelemetry metrics export.



_Appears in:_
- [DeploymentSettingsSpec](#deploymentsettingsspec)

| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `enabled` _boolean_ | Enabled defines whether to enable the metrics export or not. | false | Optional: \{\} <br /> |
| `endpoint` _string_ | Endpoint is the OpenTelemetry collector endpoint to send metrics to. | | Optional: \{\} <br /> |
| `crontab` _string_ | Crontab is the cron expression for how often to export metrics.<br />Example: "*/5 * * * *" for every 5 minutes. | | Optional: \{\} <br /> |


#### NamespaceCredentials


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ func (r *DeploymentSettingsReconciler) genDeploymentSettingsAttr(ctx context.Con
attr := &console.DeploymentSettingsAttributes{
MgmtRepo: settings.Spec.ManagementRepo,
Cost: settings.Spec.Cost.Attributes(),
Metrics: settings.Spec.Metrics.Attributes(),
}

if settings.Spec.AgentHelmValues != nil {
Expand Down
1 change: 1 addition & 0 deletions lib/console/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ defmodule Console.Application do
{Absinthe.Subscription, ConsoleWeb.Endpoint},
Console.Cached.Supervisor,
Console.Plural.Pinger,
Console.Otel.MetricsExporter,
Console.AI.GothManager,
Console.PromEx,
Console.AI.Graph.Indexer.Supervisor,
Expand Down
16 changes: 16 additions & 0 deletions lib/console/graphql/deployments/settings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,21 @@ defmodule Console.GraphQl.Deployments.Settings do

field :ai, :ai_settings_attributes, description: "configuration for LLM provider clients"
field :cost, :cost_settings_attributes, description: "settings for cost management functionality"
field :metrics, :metrics_settings_attributes, description: "settings for OpenTelemetry metrics export"

field :read_bindings, list_of(:policy_binding_attributes)
field :write_bindings, list_of(:policy_binding_attributes)
field :git_bindings, list_of(:policy_binding_attributes)
field :create_bindings, list_of(:policy_binding_attributes)
end

@desc "Settings for OpenTelemetry metrics export"
input_object :metrics_settings_attributes do
field :enabled, :boolean, description: "whether to enable metrics export"
field :endpoint, :string, description: "the OpenTelemetry collector endpoint to send metrics to"
field :crontab, :string, description: "cron expression for how often to export metrics (e.g. '*/5 * * * *')"
end

input_object :http_connection_attributes do
field :host, non_null(:string)
field :user, :string, description: "user to connect w/ for basic auth"
Expand Down Expand Up @@ -265,6 +273,7 @@ defmodule Console.GraphQl.Deployments.Settings do
field :ai, :ai_settings, description: "settings for LLM provider clients"
field :cost, :cost_settings, description: "settings for cost management"
field :logging, :logging_settings, description: "settings for connections to log aggregation datastores"
field :metrics, :metrics_settings, description: "settings for OpenTelemetry metrics export"
field :mgmt_repo, :string, description: "the root repo you used to run `plural up`"

field :onboarded, :boolean, description: "whether the console has been onboarded and getting started pages need to be shown"
Expand Down Expand Up @@ -316,6 +325,13 @@ defmodule Console.GraphQl.Deployments.Settings do
field :recommendation_cushion, :integer, description: "the percentage cushion above baseline usage to give when generation recommendations, default 20%"
end

@desc "Settings for OpenTelemetry metrics export"
object :metrics_settings do
field :enabled, :boolean, description: "whether metrics export is enabled"
field :endpoint, :string, description: "the OpenTelemetry collector endpoint"
field :crontab, :string, description: "cron expression for export schedule"
end

@desc "Settings for configuring access to common LLM providers"
object :ai_settings do
field :enabled, :boolean
Expand Down
95 changes: 95 additions & 0 deletions lib/console/otel/exporter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
defmodule Console.Otel.Exporter do
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there no library that handles this at the moment? There might not be, but would be nice to use if so

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think existing library opentelemetry_exporter is still dev status (not guaranteed to be stable)?

@moduledoc """
Simple OTLP JSON exporter for metrics over HTTP.
Sends metrics to an OpenTelemetry Collector endpoint.
"""
require Logger

@default_timeout :timer.seconds(30)

@doc """
Exports a list of metrics to the configured OTLP endpoint.
Returns :ok on success, {:error, reason} on failure.
"""
@spec export(binary, [map]) :: :ok | {:error, term}
def export(endpoint, metrics) when is_binary(endpoint) and is_list(metrics) do
url = build_url(endpoint)
payload = build_payload(metrics)

case Req.post(url, json: payload, receive_timeout: @default_timeout) do
{:ok, %Req.Response{status: status}} when status in 200..299 ->
Logger.info("Successfully exported #{length(metrics)} metrics to #{endpoint}")
:ok

{:ok, %Req.Response{status: status, body: body}} ->
Logger.error("Failed to export metrics to #{endpoint}: HTTP #{status} - #{inspect(body)}")
{:error, {:http_error, status, body}}

{:error, reason} ->
Logger.error("Failed to export metrics to #{endpoint}: #{inspect(reason)}")
{:error, reason}
end
end

defp build_url(endpoint) do
endpoint
|> String.trim_trailing("/")
|> Kernel.<>("/v1/metrics")
end

defp build_payload(metrics) do
%{
"resourceMetrics" => [
%{
"resource" => %{
"attributes" => [
%{"key" => "service.name", "value" => %{"stringValue" => "plural-console"}},
%{"key" => "service.version", "value" => %{"stringValue" => Console.conf(:version)}}
]
},
"scopeMetrics" => [
%{
"scope" => %{"name" => "plural.metrics", "version" => "1.0.0"},
"metrics" => Enum.map(metrics, &format_metric/1)
}
]
}
]
}
end

defp format_metric(%{name: name, value: value, attributes: attrs} = metric) do
timestamp = Map.get(metric, :timestamp, DateTime.utc_now())

%{
"name" => name,
"gauge" => %{
"dataPoints" => [
%{
"asInt" => value,
"timeUnixNano" => DateTime.to_unix(timestamp, :nanosecond),
"attributes" => format_attributes(attrs)
}
]
}
}
end

defp format_attributes(attrs) when is_map(attrs) do
attrs
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|> Enum.map(fn {key, value} ->
%{
"key" => to_string(key),
"value" => format_attribute_value(value)
}
end)
end

defp format_attribute_value(value) when is_binary(value), do: %{"stringValue" => value}
defp format_attribute_value(value) when is_integer(value), do: %{"intValue" => value}
defp format_attribute_value(value) when is_float(value), do: %{"doubleValue" => value}
defp format_attribute_value(value) when is_boolean(value), do: %{"boolValue" => value}
defp format_attribute_value(value) when is_atom(value), do: %{"stringValue" => to_string(value)}
defp format_attribute_value(value), do: %{"stringValue" => inspect(value)}
end
Loading
Loading