diff --git a/CHANGELOG.md b/CHANGELOG.md index 03e6af2cf..b5ba2d5e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan ### Added - [#29](https://github.com/kobsio/kobs/pull/29): Add a new dependencies section to the Application CR. These dependencies are used to show a topology graph for all Applications. +- [#31](https://github.com/kobsio/kobs/pull/31): Add plugin support for Kubernetes resources. ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c339b890..cd5356ae8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ - [Server](#server) - [Envoy](#envoy) - [Run kobs](#run-kobs) -- [Add a new Plugin](#add-a-new-plugin) +- [Documentation](#documentation) Every contribution to kobs is welcome, whether it is reporting a bug, submitting a fix, proposing new features or becoming a maintainer. To make contributing to kubenav as easy as possible you will find more details for the development flow in this documentation. @@ -155,144 +155,6 @@ When you want to run kobs inside your Kubernetes cluster, please checkout the Do **Using the demo application:** If you want to test your changes against the demo application take a look at [https://kobs.io/installation/demo/#development-using-the-demo](https://kobs.io/installation/demo/#development-using-the-demo). -## Add a new Plugin +## Documentation -To add a new plugin to kobs, you have to create a `proto/.proto`. Our [Makefile](./Makefile) will the handle the code generation for your plugin. - -```protobuf -syntax = "proto3"; -package plugins.; - -option go_package = "github.com/kobsio/kobs/pkg/api/plugins//proto"; -``` - -To add your plugin to the Application CRD, add a corresponding field to the `Plugin` message format in the `proto/plugins.proto` file: - -```protobuf -syntax = "proto3"; -package plugins; - -option go_package = "github.com/kobsio/kobs/pkg/api/plugins/plugins/proto"; - -import ".proto"; - -message Plugin { - .Spec = 1; -} -``` - -Besides the protocol buffers definition your also have to create a `pkg/api/plugins//.go` file, which implements your definition and handles the registration of your plugin. To register your plugin you have to modify the `Register` function in the `pkg/api/plugins/plugins/plugins.go` file: - -```go -package plugins - -import ( - "github.com/kobsio/kobs/pkg/api/plugins/" -) - -func Register(cfg *config.Config, grpcServer *grpc.Server) error { - Instances, err := .Register(cfg., grpcServer) - if err != nil { - log.WithError(err).WithFields(logrus.Fields{"plugin": ""}).Errorf("Failed to register plugin.") - return err - } - - plugins = append(plugins, Instances...) -} -``` - -The configuration for your plugin must be added to the `Config` struct in the `pkg/config/config.go` file: - -```go -package config - -import ( - "github.com/kobsio/kobs/pkg/api/plugins/" -) - -type Config struct { - [].Config `yaml:""` -} -``` - -Now your plugin is registered at the gRPC server and can be configured via a `config.yaml` file. In the next step you can implement the Reac UI components for your plugin. Your plugin must provide the following two components as entry point: `app/src/plugins//Page.tsx` and `app/src/plugins//Plugin.tsx`: - -```tsx -import { - PageSection, - PageSectionVariants, - Title, -} from '@patternfly/react-core'; -import React from 'react'; - -import { IPluginPageProps } from 'utils/plugins'; - -const Page: React.FunctionComponent = ({ name, description }: IPluginPageProps) => { - return ( - - - - {name} - -

{description}

-
-
- ); -}; - -export default Page; -``` - -```tsx -import React from 'react'; - -import { IPluginProps } from 'utils/plugins'; -import PluginDataMissing from 'components/plugins/PluginDataMissing'; - -const Plugin: React.FunctionComponent = ({ - name, - description, - plugin, - showDetails, -}: IPluginProps) => { - if (!plugin.) { - return ( - - ); - } - - return ( - - - ); -}; - -export default Plugin; -``` - -In the last step you have to register these two React components in the `app/src/utils/plugins.tsx` file: - -```tsx -import React from 'react'; - -import Page from 'plugins//Page'; -import Plugin from 'plugins//Plugin'; - -export const plugins: IPlugins = { - : { - page: Page, - plugin: Plugin, - }, -}; -``` - -Thats it, now you can generate the Go and TypeScript code from your `.proto` file and the new Application CRD with the following command: - -```sh -make generate -``` +More information, for example how to use the demo in you development workflow or how to submit a new plugin can be found in the documentation at [https://kobs.io/contributing/getting-started/](https://kobs.io/contributing/getting-started/). diff --git a/app/src/components/applications/ApplicationDetailsLink.tsx b/app/src/components/applications/ApplicationDetailsLink.tsx index ae17404e8..102ba9a13 100644 --- a/app/src/components/applications/ApplicationDetailsLink.tsx +++ b/app/src/components/applications/ApplicationDetailsLink.tsx @@ -21,7 +21,7 @@ const ApplicationDetailsLink: React.FunctionComponent { - setLink(`/applications/${application.cluster}/${application.namespace}/${application.name}${location.search}`); + setLink(`/applications/${application.cluster}/${application.namespace}/${application.name}`); }, [application, location.search]); return ; diff --git a/app/src/components/resources/ResourceDetails.tsx b/app/src/components/resources/ResourceDetails.tsx index 9de8750d5..0cfedbee5 100644 --- a/app/src/components/resources/ResourceDetails.tsx +++ b/app/src/components/resources/ResourceDetails.tsx @@ -1,21 +1,71 @@ import { + Alert, + AlertVariant, + Button, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, DrawerActions, DrawerCloseButton, DrawerHead, DrawerPanelBody, DrawerPanelContent, Tab, - TabContent, TabTitleText, Tabs, } from '@patternfly/react-core'; -import React, { useRef, useState } from 'react'; +import React, { useContext, useState } from 'react'; import { IRow } from '@patternfly/react-table'; +import { JSONPath } from 'jsonpath-plus'; +import { Link } from 'react-router-dom'; +import { TopologyIcon } from '@patternfly/react-icons'; import yaml from 'js-yaml'; +import { IPluginsContext, PluginsContext } from 'context/PluginsContext'; +import { applicationAnnotation, pluginAnnotation } from 'utils/constants'; import Editor from 'components/Editor'; +import { Plugin as IPlugin } from 'proto/plugins_grpc_web_pb'; +import Plugin from 'components/plugins/Plugin'; import ResourceEvents from 'components/resources/ResourceEvents'; import Title from 'components/Title'; +import { plugins as pluginsDefinition } from 'utils/plugins'; + +// interpolate is used to replace a variable ("<< $.metadata.name >>") with the result of the JSONPath from the resource +// object. This can be used to replace a query in a plugin with the name of the resource, which has the plugins +// annotation. +// To interpolate the plugins annotation we split the string by the given start pattern ("<<"), then we split the +// results again by the end pattern (">>"). The string between these patterns is then given over to our JSONPath +// function together with the complete manifest for the resource. The result of the JSONPath function is then placed at +// the position of the placeholder. Finally we have to merge everything together. +// See: https://stackoverflow.com/a/57598892/4104109 +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const interpolate = (str: string, resource: any, interpolator: string[] = ['<<', '>>']): string => { + return str + .split(interpolator[0]) + .map((s1, i) => { + if (i === 0) { + return s1; + } + + const s2 = s1.split(interpolator[1]); + if (s1 === s2[0]) { + return interpolator[0] + s2[0]; + } + + if (s2.length > 1) { + s2[0] = s2[0] ? JSONPath({ json: resource, path: s2[0].trim(), wrap: false }) : interpolator.join(''); + } + + return s2.join(''); + }) + .join(''); +}; + +interface IApplications { + namespace?: string; + name: string; +} // IResourceDetailsProps is the interface for the ResourceDetails. The component requires a resource and an function to // close the drawer panel. @@ -32,8 +82,58 @@ const ResourceDetails: React.FunctionComponent = ({ close, }: IResourceDetailsProps) => { const [activeTab, setActiveTab] = useState('yaml'); - const refYamlContent = useRef(null); - const refEventsContent = useRef(null); + const pluginsContext = useContext(PluginsContext); + + let applications: IApplications[] = []; + const plugins: IPlugin.AsObject[] = []; + let applicationsError = ''; + let pluginsError = ''; + + try { + if ( + resource.props && + resource.props.metadata && + resource.props.metadata.annotations && + resource.props.metadata.annotations[applicationAnnotation] + ) { + applications = JSON.parse(resource.props.metadata.annotations[applicationAnnotation], resource.props); + } + } catch (err) { + applicationsError = err.message; + } + + try { + if ( + resource.props && + resource.props.metadata && + resource.props.metadata.annotations && + resource.props.metadata.annotations[pluginAnnotation] + ) { + const parsedPlugins = JSON.parse( + interpolate(resource.props.metadata.annotations[pluginAnnotation], resource.props), + ); + + for (const parsedPlugin of parsedPlugins) { + if (!parsedPlugin.name) { + throw new Error('Plugin name is missing'); + } + + const pluginDetails = pluginsContext.getPluginDetails(parsedPlugin.name); + if (!pluginDetails) { + throw new Error('Plugin was not found'); + } + + const plugin = pluginsDefinition[pluginDetails.type].jsonToProto(parsedPlugin); + if (!plugin) { + throw new Error('Could not parse plugin annotation.'); + } + + plugins.push(plugin); + } + } + } catch (err) { + pluginsError = err.message; + } return ( @@ -51,52 +151,74 @@ const ResourceDetails: React.FunctionComponent = ({ +
+ {applicationsError ? ( + +

{applicationsError}

+
+ ) : applications.length > 0 ? ( + + + Applications + + {applications.map((application, index) => ( + + +
+ + ))} +
+
+
+ ) : null} +
+ setActiveTab(tabIndex.toString())} + className="pf-u-mt-md" + isFilled={true} + mountOnEnter={true} > - Yaml} - tabContentId="refYaml" - tabContentRef={refYamlContent} - /> - Events} - tabContentId="refEvents" - tabContentRef={refEventsContent} - /> + Yaml}> +
+ +
+
+ Events}> +
+ +
+
+ {pluginsError ? ( + Plugins}> +
+ +

{pluginsError}

+
+
+
+ ) : plugins.length > 0 ? ( + plugins.map((plugin, index) => ( + {plugin.name}}> +
+ +
+
+ )) + ) : null}
- - - - - -
); diff --git a/app/src/plugins/elasticsearch/helpers.ts b/app/src/plugins/elasticsearch/helpers.ts index 1dc196111..00ee13dd6 100644 --- a/app/src/plugins/elasticsearch/helpers.ts +++ b/app/src/plugins/elasticsearch/helpers.ts @@ -1,3 +1,5 @@ +import { Query, Spec } from 'proto/elasticsearch_grpc_web_pb'; +import { Plugin } from 'proto/plugins_grpc_web_pb'; import { formatTime } from 'utils/helpers'; // ITimes is the interface for a start and end time. @@ -84,3 +86,34 @@ export const getProperty = (obj: any, key: string): string | number => { export const formatTimeWrapper = (time: string): string => { return formatTime(Math.floor(new Date(time).getTime() / 1000)); }; + +// jsonToProto is used to convert a json object into the protobuf message format for the Prometheus plugin. This is +// needed, so that users can use the plugin within resources, where the plugin specs are specified as json object. +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export const jsonToProto = (json: any): Plugin.AsObject | undefined => { + if (!json.elasticsearch || !json.elasticsearch.queries || !Array.isArray(json.elasticsearch.queries)) { + return undefined; + } + + const queries: Query[] = []; + for (const query of json.elasticsearch.queries) { + if (query.name && query.query) { + const q = new Query(); + q.setName(query.name); + q.setQuery(query.query); + q.setFieldsList(query.fields && Array.isArray(query.fields) ? query.fields : []); + queries.push(q); + } else { + return undefined; + } + } + + const elasticsearch = new Spec(); + elasticsearch.setQueriesList(queries); + + const plugin = new Plugin(); + plugin.setName(json.name); + plugin.setElasticsearch(elasticsearch); + + return plugin.toObject(); +}; diff --git a/app/src/plugins/jaeger/helpers.ts b/app/src/plugins/jaeger/helpers.ts index f2f481912..8d020f76d 100644 --- a/app/src/plugins/jaeger/helpers.ts +++ b/app/src/plugins/jaeger/helpers.ts @@ -1,5 +1,7 @@ import { ChartThemeColor, getDarkThemeColors } from '@patternfly/react-charts'; +import { Query, Spec } from 'proto/jaeger_grpc_web_pb'; +import { Plugin } from 'proto/plugins_grpc_web_pb'; import { formatTime } from 'utils/helpers'; export interface IKeyValue { @@ -233,3 +235,35 @@ export const doesTraceContainsError = (trace: ITrace): boolean => { return false; }; + +// jsonToProto is used to convert a json object into the protobuf message format for the Prometheus plugin. This is +// needed, so that users can use the plugin within resources, where the plugin specs are specified as json object. +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export const jsonToProto = (json: any): Plugin.AsObject | undefined => { + if (!json.jaeger || !json.jaeger.queries || !Array.isArray(json.jaeger.queries)) { + return undefined; + } + + const queries: Query[] = []; + for (const query of json.jaeger.queries) { + if (query.name && query.service) { + const q = new Query(); + q.setName(query.name); + q.setService(query.service); + q.setOperation(query.operation ? query.operation : ''); + q.setTags(query.tags ? query.tags : ''); + queries.push(q); + } else { + return undefined; + } + } + + const jaeger = new Spec(); + jaeger.setQueriesList(queries); + + const plugin = new Plugin(); + plugin.setName(json.name); + plugin.setJaeger(jaeger); + + return plugin.toObject(); +}; diff --git a/app/src/plugins/prometheus/helpers.ts b/app/src/plugins/prometheus/helpers.ts index c1a386f61..094f3ae32 100644 --- a/app/src/plugins/prometheus/helpers.ts +++ b/app/src/plugins/prometheus/helpers.ts @@ -1,3 +1,6 @@ +import { Chart, Query, Spec, Variable } from 'proto/prometheus_grpc_web_pb'; +import { Plugin } from 'proto/plugins_grpc_web_pb'; + // ITimes is the interface for a start and end time. export interface ITimes { timeEnd: number; @@ -27,3 +30,78 @@ export const getOptionsFromSearch = (search: string): IPrometheusOptions => { timeStart: timeStart ? parseInt(timeStart as string) : Math.floor(Date.now() / 1000) - 3600, }; }; + +// jsonToProto is used to convert a json object into the protobuf message format for the Prometheus plugin. This is +// needed, so that users can use the plugin within resources, where the plugin specs are specified as json object. +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export const jsonToProto = (json: any): Plugin.AsObject | undefined => { + if (!json.prometheus) { + return undefined; + } + + const variables: Variable[] = []; + if (json.prometheus.variables && Array.isArray(json.prometheus.variables)) { + for (const variable of json.prometheus.variables) { + if (variable.name && variable.label && variable.query) { + const v = new Variable(); + v.setName(variable.name); + v.setLabel(variable.label); + v.setQuery(variable.query); + v.setAllowall(variable.allowAll ? true : false); + variables.push(v); + } + } + } + + const charts: Chart[] = []; + if (json.prometheus.charts && Array.isArray(json.prometheus.charts)) { + for (const chart of json.prometheus.charts) { + if (chart.title && chart.type) { + if (chart.type === 'divider') { + const c = new Chart(); + c.setTitle(chart.title); + c.setType(chart.type); + charts.push(c); + } else { + if (chart.queries && Array.isArray(chart.queries)) { + const c = new Chart(); + c.setTitle(chart.title); + c.setType(chart.type); + c.setUnit(chart.unit ? chart.unit : ''); + c.setStacked(chart.stacked ? true : false); + c.setSize(chart.size ? chart.size : 12); + + const queries: Query[] = []; + for (const query of chart.queries) { + if (query.query && query.label) { + const q = new Query(); + q.setLabel(query.label); + q.setQuery(query.query); + queries.push(q); + } else { + return undefined; + } + } + + c.setQueriesList(queries); + charts.push(c); + } else { + return undefined; + } + } + } else { + return undefined; + } + } + } + + const prometheus = new Spec(); + prometheus.setVariablesList(variables); + prometheus.setChartsList(charts); + + const plugin = new Plugin(); + plugin.setName(json.name); + plugin.setPrometheus(prometheus); + + return plugin.toObject(); +}; diff --git a/app/src/utils/constants.ts b/app/src/utils/constants.ts index 705de9804..4f9c0c502 100644 --- a/app/src/utils/constants.ts +++ b/app/src/utils/constants.ts @@ -10,3 +10,10 @@ export const applicationsDescription = 'Applications are a Custom Resource for k // resourcesDescription is the description, which is displayed in the UI on the resources card on the overview page // and the resources page. export const resourcesDescription = 'Resources are all available Kubernetes resources.'; + +// applicationAnnotation is the key for the annotation, which allows a user to specify a list of applications within a +// resource +export const applicationAnnotation = 'kobs.io/applications'; + +// pluginAnnotation is the key for the annotation, which allows a user to specify a list of plugins within a resource. +export const pluginAnnotation = 'kobs.io/plugins'; diff --git a/app/src/utils/plugins.tsx b/app/src/utils/plugins.tsx index 6af8c45fa..81805918f 100644 --- a/app/src/utils/plugins.tsx +++ b/app/src/utils/plugins.tsx @@ -8,9 +8,41 @@ import JaegerPlugin from 'plugins/jaeger/JaegerPlugin'; import PrometheusPage from 'plugins/prometheus/PrometheusPage'; import PrometheusPlugin from 'plugins/prometheus/PrometheusPlugin'; import PrometheusPreview from 'plugins/prometheus/PrometheusPreview'; +import { jsonToProto as elasticsearchJsonToProto } from 'plugins/elasticsearch/helpers'; +import { jsonToProto as jaegerJsonToProto } from 'plugins/jaeger/helpers'; +import { jsonToProto as prometheusJsonToProto } from 'plugins/prometheus/helpers'; import { Plugin as IProtoPlugin } from 'proto/plugins_grpc_web_pb'; +// jsonToProto is a simple json to protobuf converter function, which can be used instead of the jsonToProto function +// defined by every plugin. So that we can also try to render the plugin, when it doesn't specify a jsonToProto +// function. +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +// const jsonToProto = (json: any): IProtoPlugin.AsObject | undefined => { +// for (const [field] of Object.entries(json)) { +// if (typeof json[field] === 'object') { +// if (Array.isArray(json[field])) { +// json[`${field.toLowerCase()}List`] = jsonToProto(json[field]); +// delete json[field]; +// } else { +// if (field.toLowerCase() !== field) { +// json[field.toLowerCase()] = jsonToProto(json[field]); +// delete json[field]; +// } else { +// json[field] = jsonToProto(json[field]); +// } +// } +// } else { +// if (field.toLowerCase() !== field) { +// json[field.toLowerCase()] = json[field]; +// delete json[field]; +// } +// } +// } + +// return json as IProtoPlugin.AsObject; +// }; + // IPluginPageProps is the interface for the properties, which are passed to the page implementation of a plugin. This // is the name and the description of the plugin. export interface IPluginPageProps { @@ -31,6 +63,8 @@ export interface IPluginProps { // IPlugin is the interface for a single plugin implementation. Each plugin must implement a plugin component, which can // be used within an application and a page, which can be used to retrieve data of this plugin withour an application. export interface IPlugin { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + jsonToProto: (json: any) => IProtoPlugin.AsObject | undefined; page: React.FunctionComponent; plugin: React.FunctionComponent; preview?: React.FunctionComponent; @@ -45,15 +79,18 @@ export interface IPlugins { // plugins is the list of all supported plugins. export const plugins: IPlugins = { elasticsearch: { + jsonToProto: elasticsearchJsonToProto, page: ElasticsearchPage, plugin: ElasticsearchPlugin, preview: ElasticsearchPreview, }, jaeger: { + jsonToProto: jaegerJsonToProto, page: JaegerPage, plugin: JaegerPlugin, }, prometheus: { + jsonToProto: prometheusJsonToProto, page: PrometheusPage, plugin: PrometheusPlugin, preview: PrometheusPreview, diff --git a/deploy/demo/bookinfo/details.yaml b/deploy/demo/bookinfo/details.yaml index 1e0aa5a1c..1f83432dc 100644 --- a/deploy/demo/bookinfo/details.yaml +++ b/deploy/demo/bookinfo/details.yaml @@ -43,6 +43,56 @@ spec: labels: app: details version: v1 + annotations: + kobs.io/applications: | + [{"name": "details"}] + kobs.io/plugins: | + [ + { + "name": "Prometheus", + "prometheus": { + "variables": [ + { "name": "Container", "label": "container", "query": "container_cpu_usage_seconds_total{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container!=\"POD\", container!=\"\"}", "allowAll": true } + ], + "charts": [ + { + "title": "CPU Usage", + "type": "area", + "unit": "Cores", + "queries": [ + { "label": "Current", "query": "sum(max(rate(container_cpu_usage_seconds_total{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container=~\"{{ .Container }}\", container!=\"POD\", container!=\"\"}[2m])) by (container))" } + ] + }, + { + "title": "Memory Usage", + "type": "area", + "unit": "MiB", + "queries": [ + { "label": "Current", "query": "sum(max(rate(container_memory_usage_bytes{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container=~\"{{ .Container }}\", container!=\"POD\", container!=\"\"}[2m])) by (container)) / 1024 / 1024" } + ] + }, + { + "title": "Network I/O", + "type": "area", + "unit": "MiB", + "queries": [ + { "label": "Receive", "query": "sum(rate(container_network_receive_bytes_total{namespace=\"<< $.metadata.namespace >>\", pod=\"<< $.metadata.name >>\"}[2m])) by (pod) / 1024 / 1024" }, + { "label": "Transmit", "query": "-sum(rate(container_network_transmit_bytes_total{namespace=\"<< $.metadata.namespace >>\", pod=\"<< $.metadata.name >>\"}[2m])) by (pod) / 1024 / 1024" } + ] + } + ] + } + }, + { + "name": "Elasticsearch", + "elasticsearch": { + "queries": [ + { "name": "Container Logs: productpage", "query": "kubernetes.namespace: << $.metadata.namespace >> AND kubernetes.pod.name: << $.metadata.name >> AND kubernetes.container.name: productpage" }, + { "name": "Container Logs: istio-proxy", "query": "kubernetes.namespace: << $.metadata.namespace >> AND kubernetes.pod.name: << $.metadata.name >> AND kubernetes.container.name: istio-proxy", "fields": ["kubernetes.pod.name", "content.protocol", "content.method", "content.path", "content.response_code", "content.duration"] } + ] + } + } + ] spec: serviceAccountName: bookinfo-details containers: diff --git a/deploy/demo/bookinfo/productpage.yaml b/deploy/demo/bookinfo/productpage.yaml index 79fc6074c..ab562ef85 100644 --- a/deploy/demo/bookinfo/productpage.yaml +++ b/deploy/demo/bookinfo/productpage.yaml @@ -43,6 +43,56 @@ spec: labels: app: productpage version: v1 + annotations: + kobs.io/applications: | + [{"name": "productpage"}] + kobs.io/plugins: | + [ + { + "name": "Prometheus", + "prometheus": { + "variables": [ + { "name": "Container", "label": "container", "query": "container_cpu_usage_seconds_total{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container!=\"POD\", container!=\"\"}", "allowAll": true } + ], + "charts": [ + { + "title": "CPU Usage", + "type": "area", + "unit": "Cores", + "queries": [ + { "label": "Current", "query": "sum(max(rate(container_cpu_usage_seconds_total{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container=~\"{{ .Container }}\", container!=\"POD\", container!=\"\"}[2m])) by (container))" } + ] + }, + { + "title": "Memory Usage", + "type": "area", + "unit": "MiB", + "queries": [ + { "label": "Current", "query": "sum(max(rate(container_memory_usage_bytes{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container=~\"{{ .Container }}\", container!=\"POD\", container!=\"\"}[2m])) by (container)) / 1024 / 1024" } + ] + }, + { + "title": "Network I/O", + "type": "area", + "unit": "MiB", + "queries": [ + { "label": "Receive", "query": "sum(rate(container_network_receive_bytes_total{namespace=\"<< $.metadata.namespace >>\", pod=\"<< $.metadata.name >>\"}[2m])) by (pod) / 1024 / 1024" }, + { "label": "Transmit", "query": "-sum(rate(container_network_transmit_bytes_total{namespace=\"<< $.metadata.namespace >>\", pod=\"<< $.metadata.name >>\"}[2m])) by (pod) / 1024 / 1024" } + ] + } + ] + } + }, + { + "name": "Elasticsearch", + "elasticsearch": { + "queries": [ + { "name": "Container Logs: productpage", "query": "kubernetes.namespace: << $.metadata.namespace >> AND kubernetes.pod.name: << $.metadata.name >> AND kubernetes.container.name: productpage" }, + { "name": "Container Logs: istio-proxy", "query": "kubernetes.namespace: << $.metadata.namespace >> AND kubernetes.pod.name: << $.metadata.name >> AND kubernetes.container.name: istio-proxy", "fields": ["kubernetes.pod.name", "content.protocol", "content.method", "content.path", "content.response_code", "content.duration"] } + ] + } + } + ] spec: serviceAccountName: bookinfo-productpage containers: diff --git a/deploy/demo/bookinfo/ratings.yaml b/deploy/demo/bookinfo/ratings.yaml index 5f33d95fe..f0537c342 100644 --- a/deploy/demo/bookinfo/ratings.yaml +++ b/deploy/demo/bookinfo/ratings.yaml @@ -43,6 +43,56 @@ spec: labels: app: ratings version: v1 + annotations: + kobs.io/applications: | + [{"name": "ratings"}] + kobs.io/plugins: | + [ + { + "name": "Prometheus", + "prometheus": { + "variables": [ + { "name": "Container", "label": "container", "query": "container_cpu_usage_seconds_total{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container!=\"POD\", container!=\"\"}", "allowAll": true } + ], + "charts": [ + { + "title": "CPU Usage", + "type": "area", + "unit": "Cores", + "queries": [ + { "label": "Current", "query": "sum(max(rate(container_cpu_usage_seconds_total{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container=~\"{{ .Container }}\", container!=\"POD\", container!=\"\"}[2m])) by (container))" } + ] + }, + { + "title": "Memory Usage", + "type": "area", + "unit": "MiB", + "queries": [ + { "label": "Current", "query": "sum(max(rate(container_memory_usage_bytes{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container=~\"{{ .Container }}\", container!=\"POD\", container!=\"\"}[2m])) by (container)) / 1024 / 1024" } + ] + }, + { + "title": "Network I/O", + "type": "area", + "unit": "MiB", + "queries": [ + { "label": "Receive", "query": "sum(rate(container_network_receive_bytes_total{namespace=\"<< $.metadata.namespace >>\", pod=\"<< $.metadata.name >>\"}[2m])) by (pod) / 1024 / 1024" }, + { "label": "Transmit", "query": "-sum(rate(container_network_transmit_bytes_total{namespace=\"<< $.metadata.namespace >>\", pod=\"<< $.metadata.name >>\"}[2m])) by (pod) / 1024 / 1024" } + ] + } + ] + } + }, + { + "name": "Elasticsearch", + "elasticsearch": { + "queries": [ + { "name": "Container Logs: productpage", "query": "kubernetes.namespace: << $.metadata.namespace >> AND kubernetes.pod.name: << $.metadata.name >> AND kubernetes.container.name: productpage" }, + { "name": "Container Logs: istio-proxy", "query": "kubernetes.namespace: << $.metadata.namespace >> AND kubernetes.pod.name: << $.metadata.name >> AND kubernetes.container.name: istio-proxy", "fields": ["kubernetes.pod.name", "content.protocol", "content.method", "content.path", "content.response_code", "content.duration"] } + ] + } + } + ] spec: serviceAccountName: bookinfo-ratings containers: diff --git a/deploy/demo/bookinfo/reviews.yaml b/deploy/demo/bookinfo/reviews.yaml index d03c3acad..c4b556fb2 100644 --- a/deploy/demo/bookinfo/reviews.yaml +++ b/deploy/demo/bookinfo/reviews.yaml @@ -43,6 +43,56 @@ spec: labels: app: reviews version: v1 + annotations: + kobs.io/applications: | + [{"name": "reviews"}] + kobs.io/plugins: | + [ + { + "name": "Prometheus", + "prometheus": { + "variables": [ + { "name": "Container", "label": "container", "query": "container_cpu_usage_seconds_total{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container!=\"POD\", container!=\"\"}", "allowAll": true } + ], + "charts": [ + { + "title": "CPU Usage", + "type": "area", + "unit": "Cores", + "queries": [ + { "label": "Current", "query": "sum(max(rate(container_cpu_usage_seconds_total{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container=~\"{{ .Container }}\", container!=\"POD\", container!=\"\"}[2m])) by (container))" } + ] + }, + { + "title": "Memory Usage", + "type": "area", + "unit": "MiB", + "queries": [ + { "label": "Current", "query": "sum(max(rate(container_memory_usage_bytes{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container=~\"{{ .Container }}\", container!=\"POD\", container!=\"\"}[2m])) by (container)) / 1024 / 1024" } + ] + }, + { + "title": "Network I/O", + "type": "area", + "unit": "MiB", + "queries": [ + { "label": "Receive", "query": "sum(rate(container_network_receive_bytes_total{namespace=\"<< $.metadata.namespace >>\", pod=\"<< $.metadata.name >>\"}[2m])) by (pod) / 1024 / 1024" }, + { "label": "Transmit", "query": "-sum(rate(container_network_transmit_bytes_total{namespace=\"<< $.metadata.namespace >>\", pod=\"<< $.metadata.name >>\"}[2m])) by (pod) / 1024 / 1024" } + ] + } + ] + } + }, + { + "name": "Elasticsearch", + "elasticsearch": { + "queries": [ + { "name": "Container Logs: productpage", "query": "kubernetes.namespace: << $.metadata.namespace >> AND kubernetes.pod.name: << $.metadata.name >> AND kubernetes.container.name: productpage" }, + { "name": "Container Logs: istio-proxy", "query": "kubernetes.namespace: << $.metadata.namespace >> AND kubernetes.pod.name: << $.metadata.name >> AND kubernetes.container.name: istio-proxy", "fields": ["kubernetes.pod.name", "content.protocol", "content.method", "content.path", "content.response_code", "content.duration"] } + ] + } + } + ] spec: serviceAccountName: bookinfo-reviews containers: @@ -88,6 +138,56 @@ spec: labels: app: reviews version: v2 + annotations: + kobs.io/applications: | + [{"name": "reviews"}] + kobs.io/plugins: | + [ + { + "name": "Prometheus", + "prometheus": { + "variables": [ + { "name": "Container", "label": "container", "query": "container_cpu_usage_seconds_total{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container!=\"POD\", container!=\"\"}", "allowAll": true } + ], + "charts": [ + { + "title": "CPU Usage", + "type": "area", + "unit": "Cores", + "queries": [ + { "label": "Current", "query": "sum(max(rate(container_cpu_usage_seconds_total{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container=~\"{{ .Container }}\", container!=\"POD\", container!=\"\"}[2m])) by (container))" } + ] + }, + { + "title": "Memory Usage", + "type": "area", + "unit": "MiB", + "queries": [ + { "label": "Current", "query": "sum(max(rate(container_memory_usage_bytes{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container=~\"{{ .Container }}\", container!=\"POD\", container!=\"\"}[2m])) by (container)) / 1024 / 1024" } + ] + }, + { + "title": "Network I/O", + "type": "area", + "unit": "MiB", + "queries": [ + { "label": "Receive", "query": "sum(rate(container_network_receive_bytes_total{namespace=\"<< $.metadata.namespace >>\", pod=\"<< $.metadata.name >>\"}[2m])) by (pod) / 1024 / 1024" }, + { "label": "Transmit", "query": "-sum(rate(container_network_transmit_bytes_total{namespace=\"<< $.metadata.namespace >>\", pod=\"<< $.metadata.name >>\"}[2m])) by (pod) / 1024 / 1024" } + ] + } + ] + } + }, + { + "name": "Elasticsearch", + "elasticsearch": { + "queries": [ + { "name": "Container Logs: productpage", "query": "kubernetes.namespace: << $.metadata.namespace >> AND kubernetes.pod.name: << $.metadata.name >> AND kubernetes.container.name: productpage" }, + { "name": "Container Logs: istio-proxy", "query": "kubernetes.namespace: << $.metadata.namespace >> AND kubernetes.pod.name: << $.metadata.name >> AND kubernetes.container.name: istio-proxy", "fields": ["kubernetes.pod.name", "content.protocol", "content.method", "content.path", "content.response_code", "content.duration"] } + ] + } + } + ] spec: serviceAccountName: bookinfo-reviews containers: @@ -133,6 +233,56 @@ spec: labels: app: reviews version: v3 + annotations: + kobs.io/applications: | + [{"name": "reviews"}] + kobs.io/plugins: | + [ + { + "name": "Prometheus", + "prometheus": { + "variables": [ + { "name": "Container", "label": "container", "query": "container_cpu_usage_seconds_total{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container!=\"POD\", container!=\"\"}", "allowAll": true } + ], + "charts": [ + { + "title": "CPU Usage", + "type": "area", + "unit": "Cores", + "queries": [ + { "label": "Current", "query": "sum(max(rate(container_cpu_usage_seconds_total{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container=~\"{{ .Container }}\", container!=\"POD\", container!=\"\"}[2m])) by (container))" } + ] + }, + { + "title": "Memory Usage", + "type": "area", + "unit": "MiB", + "queries": [ + { "label": "Current", "query": "sum(max(rate(container_memory_usage_bytes{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container=~\"{{ .Container }}\", container!=\"POD\", container!=\"\"}[2m])) by (container)) / 1024 / 1024" } + ] + }, + { + "title": "Network I/O", + "type": "area", + "unit": "MiB", + "queries": [ + { "label": "Receive", "query": "sum(rate(container_network_receive_bytes_total{namespace=\"<< $.metadata.namespace >>\", pod=\"<< $.metadata.name >>\"}[2m])) by (pod) / 1024 / 1024" }, + { "label": "Transmit", "query": "-sum(rate(container_network_transmit_bytes_total{namespace=\"<< $.metadata.namespace >>\", pod=\"<< $.metadata.name >>\"}[2m])) by (pod) / 1024 / 1024" } + ] + } + ] + } + }, + { + "name": "Elasticsearch", + "elasticsearch": { + "queries": [ + { "name": "Container Logs: productpage", "query": "kubernetes.namespace: << $.metadata.namespace >> AND kubernetes.pod.name: << $.metadata.name >> AND kubernetes.container.name: productpage" }, + { "name": "Container Logs: istio-proxy", "query": "kubernetes.namespace: << $.metadata.namespace >> AND kubernetes.pod.name: << $.metadata.name >> AND kubernetes.container.name: istio-proxy", "fields": ["kubernetes.pod.name", "content.protocol", "content.method", "content.path", "content.response_code", "content.duration"] } + ] + } + } + ] spec: serviceAccountName: bookinfo-reviews containers: diff --git a/deploy/demo/observability/kobs.yaml b/deploy/demo/observability/kobs.yaml index 4365cfa8f..b1bd957ed 100644 --- a/deploy/demo/observability/kobs.yaml +++ b/deploy/demo/observability/kobs.yaml @@ -16,6 +16,8 @@ spec: - name: kobs image: "kobsio/kobs:0.1.0" imagePullPolicy: Always + args: + - --log.level=trace env: - name: KOBS_CLUSTERS_CACHE_DURATION_NAMESPACES value: 1m diff --git a/docs/contributing/add-a-plugin.md b/docs/contributing/add-a-plugin.md new file mode 100644 index 000000000..2227ea0ae --- /dev/null +++ b/docs/contributing/add-a-plugin.md @@ -0,0 +1,147 @@ +# Add a Plugin + +To add a new plugin to kobs, you have to create a `proto/.proto` file. Our [Makefile](./Makefile) will the handle the code generation for your plugin. + +```protobuf +syntax = "proto3"; +package plugins.; + +option go_package = "github.com/kobsio/kobs/pkg/api/plugins//proto"; +``` + +To add your plugin to the Application CRD, add a corresponding field to the `Plugin` message format in the `proto/plugins.proto` file: + +```protobuf +syntax = "proto3"; +package plugins; + +option go_package = "github.com/kobsio/kobs/pkg/api/plugins/plugins/proto"; + +import ".proto"; + +message Plugin { + .Spec = 1; +} +``` + +Besides the protocol buffers definition your also have to create a `pkg/api/plugins//.go` file, which implements your definition and handles the registration of your plugin. To register your plugin you have to modify the `Register` function in the `pkg/api/plugins/plugins/plugins.go` file: + +```go +package plugins + +import ( + "github.com/kobsio/kobs/pkg/api/plugins/" +) + +func Register(cfg *config.Config, grpcServer *grpc.Server) error { + Instances, err := .Register(cfg., grpcServer) + if err != nil { + log.WithError(err).WithFields(logrus.Fields{"plugin": ""}).Errorf("Failed to register plugin.") + return err + } + + plugins = append(plugins, Instances...) +} +``` + +The configuration for your plugin must be added to the `Config` struct in the `pkg/config/config.go` file: + +```go +package config + +import ( + "github.com/kobsio/kobs/pkg/api/plugins/" +) + +type Config struct { + [].Config `yaml:""` +} +``` + +Now your plugin is registered at the gRPC server and can be configured via a `config.yaml` file. In the next step you can implement the Reac UI components for your plugin. Your plugin must provide the following two components as entry point: `app/src/plugins//Page.tsx` and `app/src/plugins//Plugin.tsx`: + +```tsx +import { + PageSection, + PageSectionVariants, + Title, +} from '@patternfly/react-core'; +import React from 'react'; + +import { IPluginPageProps } from 'utils/plugins'; + +const Page: React.FunctionComponent = ({ name, description }: IPluginPageProps) => { + return ( + + + + {name} + +

{description}

+
+
+ ); +}; + +export default Page; +``` + +```tsx +import React from 'react'; + +import { IPluginProps } from 'utils/plugins'; +import PluginDataMissing from 'components/plugins/PluginDataMissing'; + +const Plugin: React.FunctionComponent = ({ + name, + description, + plugin, + showDetails, +}: IPluginProps) => { + if (!plugin.) { + return ( + + ); + } + + return ( + + + ); +}; + +export default Plugin; +``` + +In the last step you have to register these two React components in the `app/src/utils/plugins.tsx` file: + +```tsx +import React from 'react'; + +import Page from 'plugins//Page'; +import Plugin from 'plugins//Plugin'; +import Preview from 'plugins//Preview'; +import { jsonToProto as JsonToProto } from 'plugins//helpers'; + +export const plugins: IPlugins = { + : { + jsonToProto: JsonToProto, + page: Page, + plugin: Plugin, + preview: Preview, + }, +}; +``` + +The preview component is optional for every plugin. It is used in the Applications gallery, were your plugin can be used to provide additional information for an Application. You also do not have to specify a `jsonToProto` function and you can use the `jsonToProto` from the `app/src/utils/plugins.tsx` instead. In this case you have to properly check the users input, which can be avoided, by specifying an own `jsonToProto` function. + +Thats it, now you can generate the Go and TypeScript code from your `.proto` file and the new Application CRD with the following command: + +```sh +make generate +``` diff --git a/docs/contributing/development-using-the-demo.md b/docs/contributing/development-using-the-demo.md new file mode 100644 index 000000000..71b50473e --- /dev/null +++ b/docs/contributing/development-using-the-demo.md @@ -0,0 +1,31 @@ +# Development using the Demo + +The created kind cluster in the demo comes with a local registry, so that the demo can be used within your development flow. For that you have to build the Docker image with your changes and push it into the local registry: + +```sh +docker build -f ./cmd/kobs/Dockerfile -t localhost:5000/kobs:dev . +docker push localhost:5000/kobs:dev +``` + +When you have pushed your custom image, you have to adjust the patch file for the kobs deployment. Change the Docker image in the [`kobs.yaml`](https://github.com/kobsio/kobs/blob/main/deploy/demo/observability/kobs.yaml) file to the following: + +```sh +spec: + template: + spec: + containers: + - name: kobs + image: localhost:5000/kobs:dev +``` + +Now you can deploy the files for the `observability` namespace again: + +```sh +kustomize build deploy/demo/observability | kubectl apply -f - +``` + +Finally you can check if the kobs Pod is using your image, with the following command: + +```sh +k get pods -n observability -l app.kubernetes.io/name=kobs -o yaml | grep "image: localhost:5000/kobs:dev" +``` diff --git a/docs/contributing/contributing.md b/docs/contributing/getting-started.md similarity index 96% rename from docs/contributing/contributing.md rename to docs/contributing/getting-started.md index f5d66af4f..72abc318d 100644 --- a/docs/contributing/contributing.md +++ b/docs/contributing/getting-started.md @@ -1,4 +1,4 @@ -# Contributing +# Getting Started Every contribution to kobs is welcome, whether it is reporting a bug, submitting a fix, proposing new features or becoming a maintainer. To make contributing to kobs as easy as possible you will find more details for the development flow in our [Contribution Guideline](https://github.com/kobsio/kobs/blob/master/CONTRIBUTING.md). diff --git a/docs/installation/demo.md b/docs/installation/demo.md index d0d6c98a4..f649a3e8d 100644 --- a/docs/installation/demo.md +++ b/docs/installation/demo.md @@ -166,35 +166,3 @@ kind delete cluster --name kobs-demo docker stop kind-registry docker rm kind-registry ``` - -## Development using the Demo - -The created kind cluster comes with a local registry, so that the demo can be used within your development flow. For that you have to build the Docker image with your changes and push it into the local registry: - -```sh -docker build -f ./cmd/kobs/Dockerfile -t localhost:5000/kobs:dev . -docker push localhost:5000/kobs:dev -``` - -When you have pushed your custom image, you have to adjust the patch file for the kobs deployment. Change the Docker image in the [`kobs.yaml`](https://github.com/kobsio/kobs/blob/main/deploy/demo/observability/kobs.yaml) file to the following: - -```sh -spec: - template: - spec: - containers: - - name: kobs - image: localhost:5000/kobs:dev -``` - -Now you can deploy the files for the `observability` namespace again: - -```sh -kustomize build deploy/demo/observability | kubectl apply -f - -``` - -Finally you can check if the kobs Pod is using your image, with the following command: - -```sh -k get pods -n observability -l app.kubernetes.io/name=kobs -o yaml | grep "image: localhost:5000/kobs:dev" -``` diff --git a/docs/plugins/jaeger.md b/docs/plugins/jaeger.md index 42c4493e4..47e9c7b8f 100644 --- a/docs/plugins/jaeger.md +++ b/docs/plugins/jaeger.md @@ -22,7 +22,7 @@ The following specification can be used, within an application. | ----- | ---- | ----------- | -------- | | name | string | A name for the query. | Yes | | service | string | The service to retrieve traces for. | Yes | -| operations | string | An optional operation to retrieve traces for. | No | +| operation | string | An optional operation to retrieve traces for. | No | | tags | string | Tags, which the traces must be contain. | No | For example the following query specification will display all traces for the `reviews.bookinfo` service. diff --git a/docs/resources/assets/resources-plugins.png b/docs/resources/assets/resources-plugins.png new file mode 100644 index 000000000..6f1a98d7a Binary files /dev/null and b/docs/resources/assets/resources-plugins.png differ diff --git a/docs/resources/resources.md b/docs/resources/resources.md index 46dd3610f..91a548185 100644 --- a/docs/resources/resources.md +++ b/docs/resources/resources.md @@ -17,3 +17,106 @@ By selecting an item in the table, you can view some details for this resource. Next to the yaml representation, you find a seconde tab events, which shows all events, which are related to the selected object. The events are retrieved with a field selector and the name of the resource: `fieldSelector=involvedObject.name=`. ![Events](assets/resources-events.png) + +## Annotations + +You can extend your resources with additional information for kobs, by using annotations. This allows you to specify applications and plugins for your Kubernetes objects like Pods, Deployments, etc. + +| Annotations | Format | Description | +| ----------- | ------ | ----------- | +| `kobs.io/applications` | `[{"name": "application1"}, {"namespace": "namespace2", "name": "applications2"}]` | Specify a list of applications. You have to provide the name of the application and an optional namespace. If the namespace is not specified, the namespace of the resource will be used. | +| `kobs.io/plugin` | [[]Plugin](../plugins/getting-started.md#specification) | A list of plugins in the same format as it is used for applications. | + +### Applications + +Specify a list of applications within the `kobs.io/applications` annotation. The list contains multiple objects with the `name` of the application and an optional `namespace`, when the application is in another namespace then the Kubernetes resource. + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: productpage-v1 + namespace: bookinfo + labels: + app: productpage + version: v1 + annotations: + kobs.io/applications: | + [{"name": "productpage"}] +``` + +### Plugins + +Specify a list of plugins within the `kobs.io/plugins` annotation. The list contains multiple plugins, which follow the same sepecification as it is used for applications ([[]Plugin](../plugins/getting-started.md#specification)). + +Within each plugin you can use the templating feature to replace values in the annotation with values from the resource. A template starts with `<<` and end with `>>`, between the angle brackets you have to specify a [JSONPath](https://goessner.net/articles/JsonPath/). The JSONPath is run against the resource manifest and the brackets are replaced with the result of the JSONPath expression. For example the following expression will be replaced with the name of the resource: `<< $.metadata.name >>`. + +!!! note + We are using the [jsonpath-plus](https://www.npmjs.com/package/jsonpath-plus) to extract the content from the Kubernetes objects. A list of examples can be found within the documentation of the module. + +The following example adds the `kobs.io/plugins` annotation to each Pod of an Deployment. The corresponding Pods will then use the Prometheus plugin, to show the resource usage of the Pod and the Elasticsearch plugin to get all the logs for this Pod. + +```yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: productpage-v1 + namespace: bookinfo + labels: + app: productpage + version: v1 +spec: + template: + metadata: + annotations: + kobs.io/plugins: | + [ + { + "name": "Prometheus", + "prometheus": { + "variables": [ + { "name": "Container", "label": "container", "query": "container_cpu_usage_seconds_total{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container!=\"POD\", container!=\"\"}", "allowAll": true } + ], + "charts": [ + { + "title": "CPU Usage", + "type": "area", + "unit": "Cores", + "queries": [ + { "label": "Current", "query": "sum(max(rate(container_cpu_usage_seconds_total{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container=~\"{{ .Container }}\", container!=\"POD\", container!=\"\"}[2m])) by (container))" } + ] + }, + { + "title": "Memory Usage", + "type": "area", + "unit": "MiB", + "queries": [ + { "label": "Current", "query": "sum(max(rate(container_memory_usage_bytes{namespace=\"<< $.metadata.namespace >>\", image!=\"\", pod=\"<< $.metadata.name >>\", container=~\"{{ .Container }}\", container!=\"POD\", container!=\"\"}[2m])) by (container)) / 1024 / 1024" } + ] + }, + { + "title": "Network I/O", + "type": "area", + "unit": "MiB", + "queries": [ + { "label": "Receive", "query": "sum(rate(container_network_receive_bytes_total{namespace=\"<< $.metadata.namespace >>\", pod=\"<< $.metadata.name >>\"}[2m])) by (pod) / 1024 / 1024" }, + { "label": "Transmit", "query": "-sum(rate(container_network_transmit_bytes_total{namespace=\"<< $.metadata.namespace >>\", pod=\"<< $.metadata.name >>\"}[2m])) by (pod) / 1024 / 1024" } + ] + } + ] + } + }, + { + "name": "Elasticsearch", + "elasticsearch": { + "queries": [ + { "name": "Container Logs: productpage", "query": "kubernetes.namespace: << $.metadata.namespace >> AND kubernetes.pod.name: << $.metadata.name >> AND kubernetes.container.name: productpage" }, + { "name": "Container Logs: istio-proxy", "query": "kubernetes.namespace: << $.metadata.namespace >> AND kubernetes.pod.name: << $.metadata.name >> AND kubernetes.container.name: istio-proxy", "fields": ["kubernetes.pod.name", "content.protocol", "content.method", "content.path", "content.response_code", "content.duration"] } + ] + } + } + ] +``` + +![Plugins](assets/resources-plugins.png) diff --git a/mkdocs.yml b/mkdocs.yml index f864953af..93debbd08 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -34,7 +34,10 @@ nav: - Elasticsearch: plugins/elasticsearch.md - Jaeger: plugins/jaeger.md - Prometheus: plugins/prometheus.md - - Contributing: contributing/contributing.md + - Contributing: + - Getting Started: contributing/getting-started.md + - Add a Plugin: contributing/add-a-plugin.md + - Development using the Demo: contributing/development-using-the-demo.md plugins: - search