mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-12-01 14:47:38 +00:00
Merge branch 'public-single-node' into pmm-6401-read-prometheus-data-files
This commit is contained in:
commit
77a1af4f7f
27 changed files with 1379 additions and 47 deletions
|
@ -316,6 +316,7 @@ VictoriaMetrics can be used as drop-in replacement for Prometheus for scraping t
|
|||
* [kubernetes_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#kubernetes_sd_config)
|
||||
* [ec2_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#ec2_sd_config)
|
||||
* [gce_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#gce_sd_config)
|
||||
* [azure_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#azure_sd_config)
|
||||
* [consul_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#consul_sd_config)
|
||||
* [dns_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#dns_sd_config)
|
||||
* [openstack_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#openstack_sd_config)
|
||||
|
@ -2003,6 +2004,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
|||
Auth key for /debug/pprof/* endpoints. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-precisionBits int
|
||||
The number of precision bits to store per each value. Lower precision bits improves data compression at the cost of precision loss (default 64)
|
||||
-promscrape.azureSDCheckInterval duration
|
||||
Interval for checking for changes in Azure. This works only if azure_sd_configs is configured in '-promscrape.config' file. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#azure_sd_config for details (default 1m0s)
|
||||
-promscrape.cluster.memberNum string
|
||||
The number of number in the cluster of scrapers. It must be an unique value in the range 0 ... promscrape.cluster.membersCount-1 across scrapers in the cluster. Can be specified as pod name of Kubernetes StatefulSet - pod-name-Num, where Num is a numeric part of pod name (default "0")
|
||||
-promscrape.cluster.membersCount int
|
||||
|
|
|
@ -166,6 +166,8 @@ The following scrape types in [scrape_config](https://prometheus.io/docs/prometh
|
|||
* if `zone` arg is missing then `vmagent` uses the zone for the instance where it runs;
|
||||
* if `zone` arg equals to `"*"`, then `vmagent` discovers all the zones for the given project;
|
||||
* `zone` may contain a list of zones, i.e. `zone: [us-east1-a, us-east1-b]`.
|
||||
* `azure_sd_configs` - is for scraping the targets registered in Azure Cloud.
|
||||
See [azure_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#azure_sd_config) for details.
|
||||
* `consul_sd_configs` is for discovering and scraping targets registered in Consul. See [consul_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#consul_sd_config) for details.
|
||||
* `dns_sd_configs` is for discovering and scraping targets from DNS records (SRV, A and AAAA). See [dns_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#dns_sd_config) for details.
|
||||
* `openstack_sd_configs` is for discovering and scraping OpenStack targets. See [openstack_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#openstack_sd_config) for details. [OpenStack identity API v3](https://docs.openstack.org/api-ref/identity/v3/) is supported only.
|
||||
|
@ -998,6 +1000,8 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
|||
Trim timestamps for OpenTSDB HTTP data to this duration. Minimum practical duration is 1ms. Higher duration (i.e. 1s) may be used for reducing disk space usage for timestamp data (default 1ms)
|
||||
-pprofAuthKey string
|
||||
Auth key for /debug/pprof/* endpoints. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-promscrape.azureSDCheckInterval duration
|
||||
Interval for checking for changes in Azure. This works only if azure_sd_configs is configured in '-promscrape.config' file. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#azure_sd_config for details (default 1m0s)
|
||||
-promscrape.cluster.memberNum string
|
||||
The number of number in the cluster of scrapers. It must be an unique value in the range 0 ... promscrape.cluster.membersCount-1 across scrapers in the cluster. Can be specified as pod name of Kubernetes StatefulSet - pod-name-Num, where Num is a numeric part of pod name (default "0")
|
||||
-promscrape.cluster.membersCount int
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.7e6d0c89.css",
|
||||
"main.js": "./static/js/main.6cbf53db.js",
|
||||
"main.js": "./static/js/main.a6398eac.js",
|
||||
"static/js/27.939f971b.chunk.js": "./static/js/27.939f971b.chunk.js",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.7e6d0c89.css",
|
||||
"static/js/main.6cbf53db.js"
|
||||
"static/js/main.a6398eac.js"
|
||||
]
|
||||
}
|
|
@ -1 +1 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.6cbf53db.js"></script><link href="./static/css/main.7e6d0c89.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.a6398eac.js"></script><link href="./static/css/main.7e6d0c89.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
File diff suppressed because one or more lines are too long
2
app/vmselect/vmui/static/js/main.a6398eac.js
Normal file
2
app/vmselect/vmui/static/js/main.a6398eac.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.18.3 as build-web-stage
|
||||
FROM golang:1.18.4 as build-web-stage
|
||||
COPY build /build
|
||||
|
||||
WORKDIR /build
|
||||
|
|
|
@ -7,6 +7,7 @@ import Box from "@mui/material/Box";
|
|||
import Paper from "@mui/material/Paper";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import MenuList from "@mui/material/MenuList";
|
||||
import ClickAwayListener from "@mui/material/ClickAwayListener";
|
||||
|
||||
export interface QueryEditorProps {
|
||||
setHistoryIndex: (step: number, index: number) => void;
|
||||
|
@ -37,11 +38,14 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
|||
const [focusOption, setFocusOption] = useState(-1);
|
||||
const autocompleteAnchorEl = useRef<HTMLDivElement>(null);
|
||||
const wrapperEl = useRef<HTMLUListElement>(null);
|
||||
const [openAutocomplete, setOpenAutocomplete] = useState(false);
|
||||
|
||||
const openAutocomplete = useMemo(() => {
|
||||
useEffect(() => {
|
||||
if (!focusField) return;
|
||||
const words = (query.match(/[a-zA-Z_:.][a-zA-Z0-9_:.]*/gm) || []).length;
|
||||
return !(!autocomplete || query.length < 2 || words > 1 || !focusField);
|
||||
}, [query, autocomplete, focusField]);
|
||||
setOpenAutocomplete(!(!autocomplete || query.length < 2 || words > 1));
|
||||
},
|
||||
[autocomplete, query]);
|
||||
|
||||
const actualOptions = useMemo(() => {
|
||||
setFocusOption(0);
|
||||
|
@ -106,28 +110,28 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
|||
focused={!!query}
|
||||
error={!!error}
|
||||
onFocus={() => setFocusField(true)}
|
||||
onBlur={(e) => {
|
||||
const autocompleteItem = e.relatedTarget?.id || "";
|
||||
const itemIndex = actualOptions.indexOf(autocompleteItem.replace("$autocomplete$", ""));
|
||||
if (itemIndex !== -1) {
|
||||
setQuery(actualOptions[itemIndex], index);
|
||||
e.target.focus();
|
||||
} else {
|
||||
setFocusField(false);
|
||||
}
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={(e) => setQuery(e.target.value, index)}
|
||||
/>
|
||||
<Popper open={openAutocomplete} anchorEl={autocompleteAnchorEl.current} placement="bottom-start">
|
||||
<Paper elevation={3} sx={{ maxHeight: 300, overflow: "auto" }}>
|
||||
<MenuList ref={wrapperEl} dense>
|
||||
{actualOptions.map((item, i) =>
|
||||
<MenuItem id={`$autocomplete$${item}`} key={item} sx={{bgcolor: `rgba(0, 0, 0, ${i === focusOption ? 0.12 : 0})`}}>
|
||||
{item}
|
||||
</MenuItem>)}
|
||||
</MenuList>
|
||||
</Paper>
|
||||
<Popper open={openAutocomplete} anchorEl={autocompleteAnchorEl.current} placement="bottom-start" sx={{zIndex: 3}}>
|
||||
<ClickAwayListener onClickAway={() => setOpenAutocomplete(false)}>
|
||||
<Paper elevation={3} sx={{ maxHeight: 300, overflow: "auto" }}>
|
||||
<MenuList ref={wrapperEl} dense>
|
||||
{actualOptions.map((item, i) =>
|
||||
<MenuItem
|
||||
id={`$autocomplete$${item}`}
|
||||
key={item}
|
||||
sx={{bgcolor: `rgba(0, 0, 0, ${i === focusOption ? 0.12 : 0})`}}
|
||||
onClick={() => {
|
||||
setQuery(item, index);
|
||||
setOpenAutocomplete(false);
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</MenuItem>)}
|
||||
</MenuList>
|
||||
</Paper>
|
||||
</ClickAwayListener>
|
||||
</Popper>
|
||||
</Box>;
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, {FC, useEffect, useState, useMemo} from "preact/compat";
|
||||
import {KeyboardEvent} from "react";
|
||||
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
|
||||
import {dateFromSeconds, formatDateForNativeInput} from "../../../../utils/time";
|
||||
import TimeDurationSelector from "./TimeDurationSelector";
|
||||
|
@ -13,6 +14,7 @@ import Paper from "@mui/material/Paper";
|
|||
import Divider from "@mui/material/Divider";
|
||||
import ClickAwayListener from "@mui/material/ClickAwayListener";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import AlarmAdd from "@mui/icons-material/AlarmAdd";
|
||||
|
||||
const formatDate = "YYYY-MM-DD HH:mm:ss";
|
||||
|
||||
|
@ -35,6 +37,7 @@ const classes = {
|
|||
|
||||
export const TimeSelector: FC = () => {
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
const [until, setUntil] = useState<string>();
|
||||
const [from, setFrom] = useState<string>();
|
||||
|
||||
|
@ -63,8 +66,30 @@ export const TimeSelector: FC = () => {
|
|||
};
|
||||
}, [start, end]);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const setTimeAndClosePicker = () => {
|
||||
if (from) {
|
||||
dispatch({type: "SET_FROM", payload: new Date(from)});
|
||||
}
|
||||
if (until) {
|
||||
dispatch({type: "SET_UNTIL", payload: new Date(until)});
|
||||
}
|
||||
setAnchorEl(null);
|
||||
};
|
||||
const onFromChange = (from: dayjs.Dayjs | null) => setFrom(from?.format(formatDate));
|
||||
const onUntilChange = (until: dayjs.Dayjs | null) => setUntil(until?.format(formatDate));
|
||||
const onApplyClick = () => setTimeAndClosePicker();
|
||||
const onSwitchToNow = () => dispatch({type: "RUN_QUERY_TO_NOW"});
|
||||
const onCancelClick = () => {
|
||||
setUntil(formatDateForNativeInput(dateFromSeconds(end)));
|
||||
setFrom(formatDateForNativeInput(dateFromSeconds(start)));
|
||||
setAnchorEl(null);
|
||||
};
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter" || e.keyCode === 13) {
|
||||
setTimeAndClosePicker();
|
||||
}
|
||||
};
|
||||
|
||||
return <>
|
||||
<Tooltip title="Time range controls">
|
||||
|
@ -95,11 +120,11 @@ export const TimeSelector: FC = () => {
|
|||
label="From"
|
||||
ampm={false}
|
||||
value={from}
|
||||
onChange={date => date && dispatch({type: "SET_FROM", payload: date as unknown as Date})}
|
||||
onChange={onFromChange}
|
||||
onError={console.log}
|
||||
inputFormat={formatDate}
|
||||
mask="____-__-__ __:__:__"
|
||||
renderInput={(params) => <TextField {...params} variant="standard"/>}
|
||||
renderInput={(params) => <TextField {...params} variant="standard" onKeyDown={onKeyDown}/>}
|
||||
maxDate={dayjs(until)}
|
||||
PopperProps={{disablePortal: true}}/>
|
||||
</Box>
|
||||
|
@ -108,18 +133,21 @@ export const TimeSelector: FC = () => {
|
|||
label="To"
|
||||
ampm={false}
|
||||
value={until}
|
||||
onChange={date => date && dispatch({type: "SET_UNTIL", payload: date as unknown as Date})}
|
||||
onChange={onUntilChange}
|
||||
onError={console.log}
|
||||
inputFormat={formatDate}
|
||||
mask="____-__-__ __:__:__"
|
||||
renderInput={(params) => <TextField {...params} variant="standard"/>}
|
||||
renderInput={(params) => <TextField {...params} variant="standard" onKeyDown={onKeyDown}/>}
|
||||
PopperProps={{disablePortal: true}}/>
|
||||
</Box>
|
||||
<Box display="grid" gridTemplateColumns="auto 1fr" gap={1}>
|
||||
<Button variant="outlined" onClick={() => setAnchorEl(null)}>
|
||||
<Button variant="outlined" onClick={onCancelClick}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" onClick={() => dispatch({type: "RUN_QUERY_TO_NOW"})}>
|
||||
<Button variant="outlined" onClick={onApplyClick} color={"success"}>
|
||||
Apply
|
||||
</Button>
|
||||
<Button startIcon={<AlarmAdd />} onClick={onSwitchToNow}>
|
||||
switch to now
|
||||
</Button>
|
||||
</Box>
|
||||
|
|
|
@ -15,9 +15,11 @@ import Spinner from "../common/Spinner";
|
|||
import {useFetchQueryOptions} from "../../hooks/useFetchQueryOptions";
|
||||
import TracingsView from "./Views/TracingsView";
|
||||
import Trace from "./Trace/Trace";
|
||||
import TableSettings from "../Table/TableSettings";
|
||||
|
||||
const CustomPanel: FC = () => {
|
||||
|
||||
const [displayColumns, setDisplayColumns] = useState<string[]>();
|
||||
const [tracesState, setTracesState] = useState<Trace[]>([]);
|
||||
const {displayType, time: {period}, query, queryControls: {isTracingEnabled}} = useAppState();
|
||||
const { customStep, yaxis } = useGraphState();
|
||||
|
@ -73,6 +75,11 @@ const CustomPanel: FC = () => {
|
|||
setYaxisLimits={setYaxisLimits}
|
||||
toggleEnableLimits={toggleEnableLimits}
|
||||
/>}
|
||||
{displayType === "table" && <TableSettings
|
||||
data={liveData || []}
|
||||
defaultColumns={displayColumns}
|
||||
onChange={setDisplayColumns}
|
||||
/>}
|
||||
</Box>
|
||||
</Box>
|
||||
{error && <Alert color="error" severity="error" sx={{whiteSpace: "pre-wrap", mt: 2}}>{error}</Alert>}
|
||||
|
@ -90,7 +97,7 @@ const CustomPanel: FC = () => {
|
|||
traces={tracesState}
|
||||
onDeleteClick={handleTraceDelete}
|
||||
/>}
|
||||
<TableView data={liveData}/>
|
||||
<TableView data={liveData} displayColumns={displayColumns}/>
|
||||
</>}
|
||||
</Box>}
|
||||
</Box>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, {FC, useMemo, useState} from "preact/compat";
|
||||
import React, {FC, useEffect, useMemo, useRef, useState} from "preact/compat";
|
||||
import {InstantMetricResult} from "../../../api/types";
|
||||
import {InstantDataSeries} from "../../../types";
|
||||
import Table from "@mui/material/Table";
|
||||
|
@ -10,14 +10,16 @@ import TableRow from "@mui/material/TableRow";
|
|||
import TableSortLabel from "@mui/material/TableSortLabel";
|
||||
import {useSortedCategories} from "../../../hooks/useSortedCategories";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import {useAppState} from "../../../state/common/StateContext";
|
||||
|
||||
export interface GraphViewProps {
|
||||
data: InstantMetricResult[];
|
||||
displayColumns?: string[]
|
||||
}
|
||||
|
||||
const TableView: FC<GraphViewProps> = ({data}) => {
|
||||
const TableView: FC<GraphViewProps> = ({data, displayColumns}) => {
|
||||
|
||||
const sortedColumns = useSortedCategories(data);
|
||||
const sortedColumns = useSortedCategories(data, displayColumns);
|
||||
|
||||
const [orderBy, setOrderBy] = useState("");
|
||||
const [orderDir, setOrderDir] = useState<"asc" | "desc">("asc");
|
||||
|
@ -43,16 +45,24 @@ const TableView: FC<GraphViewProps> = ({data}) => {
|
|||
setOrderBy(key);
|
||||
};
|
||||
|
||||
const {query} = useAppState();
|
||||
const [tableContainerHeight, setTableContainerHeight] = useState("");
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (!tableContainerRef.current) return;
|
||||
const {top} = tableContainerRef.current.getBoundingClientRect();
|
||||
setTableContainerHeight(`calc(100vh - ${top + 32}px)`);
|
||||
}, [tableContainerRef, query]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(rows.length > 0)
|
||||
? <TableContainer>
|
||||
<Table aria-label="simple table">
|
||||
? <TableContainer ref={tableContainerRef} sx={{width: "calc(100vw - 68px)", height: tableContainerHeight}}>
|
||||
<Table stickyHeader aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{sortedColumns.map((col, index) => (
|
||||
<TableCell key={index} style={{textTransform: "capitalize"}}>
|
||||
<TableCell key={index} style={{textTransform: "capitalize", paddingTop: 0}}>
|
||||
<TableSortLabel
|
||||
active={orderBy === col.key}
|
||||
direction={orderDir}
|
||||
|
@ -79,7 +89,9 @@ const TableView: FC<GraphViewProps> = ({data}) => {
|
|||
{row.metadata.map((rowMeta, index2) => {
|
||||
const prevRowValue = rows[index - 1] && rows[index - 1].metadata[index2];
|
||||
return (
|
||||
<TableCell sx={prevRowValue === rowMeta ? {opacity: 0.4} : {}}
|
||||
<TableCell
|
||||
sx={prevRowValue === rowMeta ? {opacity: 0.4} : {}}
|
||||
style={{whiteSpace: "nowrap"}}
|
||||
key={index2}>{rowMeta}</TableCell>
|
||||
);
|
||||
}
|
||||
|
|
135
app/vmui/packages/vmui/src/components/Table/TableSettings.tsx
Normal file
135
app/vmui/packages/vmui/src/components/Table/TableSettings.tsx
Normal file
|
@ -0,0 +1,135 @@
|
|||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import React, {FC, useEffect, useState} from "preact/compat";
|
||||
import Box from "@mui/material/Box";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Popper from "@mui/material/Popper";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import ClickAwayListener from "@mui/material/ClickAwayListener";
|
||||
import {useSortedCategories} from "../../hooks/useSortedCategories";
|
||||
import {InstantMetricResult} from "../../api/types";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import {FormGroup, FormLabel} from "@mui/material";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import Button from "@mui/material/Button";
|
||||
|
||||
const classes = {
|
||||
popover: {
|
||||
display: "grid",
|
||||
gridGap: "16px",
|
||||
padding: "0 0 25px",
|
||||
},
|
||||
popoverHeader: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
background: "#3F51B5",
|
||||
padding: "6px 6px 6px 12px",
|
||||
borderRadius: "4px 4px 0 0",
|
||||
color: "#FFF",
|
||||
},
|
||||
popoverBody: {
|
||||
display: "grid",
|
||||
gridGap: "6px",
|
||||
padding: "0 14px",
|
||||
minWidth: "200px",
|
||||
}
|
||||
};
|
||||
|
||||
const title = "Table Settings";
|
||||
|
||||
interface TableSettingsProps {
|
||||
data: InstantMetricResult[];
|
||||
defaultColumns?: string[]
|
||||
onChange: (arr: string[]) => void
|
||||
}
|
||||
|
||||
const TableSettings: FC<TableSettingsProps> = ({data, defaultColumns, onChange}) => {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const columns = useSortedCategories(data);
|
||||
const [checkedColumns, setCheckedColumns] = useState(columns.map(col => col.key));
|
||||
|
||||
const handleChange = (key: string) => {
|
||||
setCheckedColumns(prev => checkedColumns.includes(key) ? prev.filter(col => col !== key) : [...prev, key]);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
setCheckedColumns(defaultColumns || columns.map(col => col.key));
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setAnchorEl(null);
|
||||
const value = columns.map(col => col.key);
|
||||
setCheckedColumns(value);
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
const handleApply = () => {
|
||||
setAnchorEl(null);
|
||||
onChange(checkedColumns);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCheckedColumns(columns.map(col => col.key));
|
||||
}, [columns]);
|
||||
|
||||
return <Box>
|
||||
<Tooltip title={title}>
|
||||
<IconButton onClick={(e) => setAnchorEl(e.currentTarget)}>
|
||||
<SettingsIcon/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Popper
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
placement="left-start"
|
||||
sx={{zIndex: 3}}
|
||||
modifiers={[{name: "offset", options: {offset: [0, 6]}}]}>
|
||||
<ClickAwayListener onClickAway={() => handleClose()}>
|
||||
<Paper elevation={3} sx={classes.popover}>
|
||||
<Box id="handle" sx={classes.popoverHeader}>
|
||||
<Typography variant="body1"><b>{title}</b></Typography>
|
||||
<IconButton size="small" onClick={() => handleClose()}>
|
||||
<CloseIcon style={{color: "white"}}/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box sx={classes.popoverBody}>
|
||||
<FormControl component="fieldset" variant="standard">
|
||||
<FormLabel component="legend">Display columns</FormLabel>
|
||||
<FormGroup sx={{display: "grid", maxHeight: "350px", overflow: "auto"}}>
|
||||
{columns.map(col => (
|
||||
<FormControlLabel
|
||||
key={col.key}
|
||||
label={col.key}
|
||||
sx={{textTransform: "capitalize"}}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={checkedColumns.includes(col.key)}
|
||||
onChange={() => handleChange(col.key)}
|
||||
name={col.key} />
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
</FormControl>
|
||||
<Box display="grid" gridTemplateColumns="1fr 1fr" gap={1} justifyContent="center" mt={2}>
|
||||
<Button variant="outlined" onClick={handleReset}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button variant="contained" onClick={handleApply}>
|
||||
apply
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</ClickAwayListener>
|
||||
</Popper>
|
||||
</Box>;
|
||||
};
|
||||
|
||||
export default TableSettings;
|
|
@ -6,7 +6,7 @@ export type MetricCategory = {
|
|||
variations: number;
|
||||
}
|
||||
|
||||
export const useSortedCategories = (data: MetricBase[]): MetricCategory[] => useMemo(() => {
|
||||
export const useSortedCategories = (data: MetricBase[], displayColumns?: string[]): MetricCategory[] => useMemo(() => {
|
||||
const columns: { [key: string]: { options: Set<string> } } = {};
|
||||
data.forEach(d =>
|
||||
Object.entries(d.metric).forEach(e =>
|
||||
|
@ -14,8 +14,10 @@ export const useSortedCategories = (data: MetricBase[]): MetricCategory[] => us
|
|||
)
|
||||
);
|
||||
|
||||
return Object.entries(columns).map(e => ({
|
||||
const sortedColumns = Object.entries(columns).map(e => ({
|
||||
key: e[0],
|
||||
variations: e[1].options.size
|
||||
})).sort((a1, a2) => a1.variations - a2.variations);
|
||||
}, [data]);
|
||||
|
||||
return displayColumns ? sortedColumns.filter(col => displayColumns.includes(col.key)) : sortedColumns;
|
||||
}, [data, displayColumns]);
|
||||
|
|
|
@ -4,7 +4,7 @@ DOCKER_NAMESPACE := victoriametrics
|
|||
|
||||
ROOT_IMAGE ?= alpine:3.16.0
|
||||
CERTS_IMAGE := alpine:3.16.0
|
||||
GO_BUILDER_IMAGE := golang:1.18.3-alpine
|
||||
GO_BUILDER_IMAGE := golang:1.18.4-alpine
|
||||
BUILDER_IMAGE := local/builder:2.0.0-$(shell echo $(GO_BUILDER_IMAGE) | tr :/ __)-1
|
||||
BASE_IMAGE := local/base:1.1.3-$(shell echo $(ROOT_IMAGE) | tr :/ __)-$(shell echo $(CERTS_IMAGE) | tr :/ __)
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
|
|||
**Update note 2:** [vmalert](https://docs.victoriametrics.com/vmalert.html) adds `/vmalert/` prefix to [web urls](https://docs.victoriametrics.com/vmalert.html#web) according to [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2825). This may affect `vmalert` instances with non-empty `-http.pathPrefix` command-line flag. After the update, configuring this flag is no longer needed. Here's [why](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2799#issuecomment-1171392005).
|
||||
**Update note 3:** this release introduces backwards-incompatible changes to communication protocol between `vmselect` and `vmstorage` nodes in cluster version of VictoriaMetrics because of added ability to query `vmselect` data from other `vmselect` nodes - see [these docs](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#multi-level-cluster-setup), so read requests to `vmselect` will fail until the upgrade is complete. These errors will stop after all the `vmselect` and `vmstorage` nodes are updated to the new release. It is safe to downgrade to previous releases at any time.
|
||||
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): add [azure_sd_configs](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#azure_sd_config) service discovery mechanism. It allows discovering Virtual Machines at [Azure Cloud](https://azure.microsoft.com/en-us/). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1364).
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): deprecate alert's status link `/api/v1/<groupID>/<alertID>/status` in favour of `api/v1/alert?group_id=<group_id>&alert_id=<alert_id>"`. The old alert's status link is still supported, but will be removed in future releases. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2825).
|
||||
* FEATURE: [cluster version of VictoriaMetrics](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html): add support for querying lower-level `vmselect` nodes from upper-level `vmselect` nodes. This makes possible to build multi-level cluster setups for global querying view and HA purposes without the need to use [Promxy](https://github.com/jacksontj/promxy). See [these docs](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#multi-level-cluster-setup) and [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2778).
|
||||
* FEATURE: add `-search.setLookbackToStep` command-line flag, which enables InfluxDB-like gap filling during querying. See [these docs](https://docs.victoriametrics.com/guides/migrate-from-influx.html) for details.
|
||||
|
@ -52,6 +53,7 @@ scrape_configs:
|
|||
* `vm_series_read_per_query` - the number of series read per query.
|
||||
|
||||
* FEATURE: publish binaries for FreeBSD and OpenBSD at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): allow selecting the needed columns at table view. This functionaly may help when the selected time series contain many different labels. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2817) and [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2867).
|
||||
|
||||
* BUGFIX: consistently name binaries at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) in the form `$(APP_NAME)-$(GOOS)-$(GOARCH)-$(VERSION).tar.gz`. For example, `victoria-metrics-linux-amd64-v1.79.0.tar.gz`. Previously the `$(GOOS)` part was missing in binaries for Linux.
|
||||
* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): allow using `__name__` label (aka [metric name](https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors)) in alerting annotations. For example:
|
||||
|
@ -74,6 +76,8 @@ scrape_configs:
|
|||
* BUGFIX: [vmselect](https://docs.victoriametrics.com/#vmselect): update `vm_partial_results_total` metric labels to be consistent with `vm_requests_total` labels.
|
||||
* BUGFIX: accept tags without values when reading data in [DataDog format](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#how-to-send-data-from-datadog-agent). Thanks to @PerGon for the [pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2839).
|
||||
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): properly pass the end of the selected time range to `time` query arg to [/api/v1/query](https://prometheus.io/docs/prometheus/latest/querying/api/#instant-queries) when displaying the requested data in JSON and table views. Previously the `time` query arg wasn't set, so `/api/v1/query` was always returning query results for the current time regardless of the selected time range. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2781).
|
||||
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): allow clicking on the suggestion from autocomplete list. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2804).
|
||||
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): apply the selected time range in date picker only after clicking the `Apply` button. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2811).
|
||||
|
||||
## [v1.78.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.78.1)
|
||||
|
||||
|
|
|
@ -316,6 +316,7 @@ VictoriaMetrics can be used as drop-in replacement for Prometheus for scraping t
|
|||
* [kubernetes_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#kubernetes_sd_config)
|
||||
* [ec2_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#ec2_sd_config)
|
||||
* [gce_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#gce_sd_config)
|
||||
* [azure_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#azure_sd_config)
|
||||
* [consul_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#consul_sd_config)
|
||||
* [dns_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#dns_sd_config)
|
||||
* [openstack_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#openstack_sd_config)
|
||||
|
@ -2003,6 +2004,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
|||
Auth key for /debug/pprof/* endpoints. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-precisionBits int
|
||||
The number of precision bits to store per each value. Lower precision bits improves data compression at the cost of precision loss (default 64)
|
||||
-promscrape.azureSDCheckInterval duration
|
||||
Interval for checking for changes in Azure. This works only if azure_sd_configs is configured in '-promscrape.config' file. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#azure_sd_config for details (default 1m0s)
|
||||
-promscrape.cluster.memberNum string
|
||||
The number of number in the cluster of scrapers. It must be an unique value in the range 0 ... promscrape.cluster.membersCount-1 across scrapers in the cluster. Can be specified as pod name of Kubernetes StatefulSet - pod-name-Num, where Num is a numeric part of pod name (default "0")
|
||||
-promscrape.cluster.membersCount int
|
||||
|
|
|
@ -320,6 +320,7 @@ VictoriaMetrics can be used as drop-in replacement for Prometheus for scraping t
|
|||
* [kubernetes_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#kubernetes_sd_config)
|
||||
* [ec2_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#ec2_sd_config)
|
||||
* [gce_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#gce_sd_config)
|
||||
* [azure_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#azure_sd_config)
|
||||
* [consul_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#consul_sd_config)
|
||||
* [dns_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#dns_sd_config)
|
||||
* [openstack_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#openstack_sd_config)
|
||||
|
@ -2007,6 +2008,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
|||
Auth key for /debug/pprof/* endpoints. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-precisionBits int
|
||||
The number of precision bits to store per each value. Lower precision bits improves data compression at the cost of precision loss (default 64)
|
||||
-promscrape.azureSDCheckInterval duration
|
||||
Interval for checking for changes in Azure. This works only if azure_sd_configs is configured in '-promscrape.config' file. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#azure_sd_config for details (default 1m0s)
|
||||
-promscrape.cluster.memberNum string
|
||||
The number of number in the cluster of scrapers. It must be an unique value in the range 0 ... promscrape.cluster.membersCount-1 across scrapers in the cluster. Can be specified as pod name of Kubernetes StatefulSet - pod-name-Num, where Num is a numeric part of pod name (default "0")
|
||||
-promscrape.cluster.membersCount int
|
||||
|
|
|
@ -170,6 +170,8 @@ The following scrape types in [scrape_config](https://prometheus.io/docs/prometh
|
|||
* if `zone` arg is missing then `vmagent` uses the zone for the instance where it runs;
|
||||
* if `zone` arg equals to `"*"`, then `vmagent` discovers all the zones for the given project;
|
||||
* `zone` may contain a list of zones, i.e. `zone: [us-east1-a, us-east1-b]`.
|
||||
* `azure_sd_configs` - is for scraping the targets registered in Azure Cloud.
|
||||
See [azure_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#azure_sd_config) for details.
|
||||
* `consul_sd_configs` is for discovering and scraping targets registered in Consul. See [consul_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#consul_sd_config) for details.
|
||||
* `dns_sd_configs` is for discovering and scraping targets from DNS records (SRV, A and AAAA). See [dns_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#dns_sd_config) for details.
|
||||
* `openstack_sd_configs` is for discovering and scraping OpenStack targets. See [openstack_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#openstack_sd_config) for details. [OpenStack identity API v3](https://docs.openstack.org/api-ref/identity/v3/) is supported only.
|
||||
|
@ -1002,6 +1004,8 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
|||
Trim timestamps for OpenTSDB HTTP data to this duration. Minimum practical duration is 1ms. Higher duration (i.e. 1s) may be used for reducing disk space usage for timestamp data (default 1ms)
|
||||
-pprofAuthKey string
|
||||
Auth key for /debug/pprof/* endpoints. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-promscrape.azureSDCheckInterval duration
|
||||
Interval for checking for changes in Azure. This works only if azure_sd_configs is configured in '-promscrape.config' file. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#azure_sd_config for details (default 1m0s)
|
||||
-promscrape.cluster.memberNum string
|
||||
The number of number in the cluster of scrapers. It must be an unique value in the range 0 ... promscrape.cluster.membersCount-1 across scrapers in the cluster. Can be specified as pod name of Kubernetes StatefulSet - pod-name-Num, where Num is a numeric part of pod name (default "0")
|
||||
-promscrape.cluster.membersCount int
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/azure"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/consul"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/digitalocean"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/dns"
|
||||
|
@ -228,6 +229,7 @@ type ScrapeConfig struct {
|
|||
MetricRelabelConfigs []promrelabel.RelabelConfig `yaml:"metric_relabel_configs,omitempty"`
|
||||
SampleLimit int `yaml:"sample_limit,omitempty"`
|
||||
|
||||
AzureSDConfigs []azure.SDConfig `yaml:"azure_sd_configs,omitempty"`
|
||||
ConsulSDConfigs []consul.SDConfig `yaml:"consul_sd_configs,omitempty"`
|
||||
DigitaloceanSDConfigs []digitalocean.SDConfig `yaml:"digitalocean_sd_configs,omitempty"`
|
||||
DNSSDConfigs []dns.SDConfig `yaml:"dns_sd_configs,omitempty"`
|
||||
|
@ -273,6 +275,9 @@ func (sc *ScrapeConfig) mustStart(baseDir string) {
|
|||
}
|
||||
|
||||
func (sc *ScrapeConfig) mustStop() {
|
||||
for i := range sc.AzureSDConfigs {
|
||||
sc.AzureSDConfigs[i].MustStop()
|
||||
}
|
||||
for i := range sc.ConsulSDConfigs {
|
||||
sc.ConsulSDConfigs[i].MustStop()
|
||||
}
|
||||
|
@ -450,6 +455,33 @@ func getSWSByJob(sws []*ScrapeWork) map[string][]*ScrapeWork {
|
|||
return m
|
||||
}
|
||||
|
||||
// getAzureSDScrapeWork returns `azure_sd_configs` ScrapeWork from cfg.
|
||||
func (cfg *Config) getAzureSDScrapeWork(prev []*ScrapeWork) []*ScrapeWork {
|
||||
swsPrevByJob := getSWSByJob(prev)
|
||||
dst := make([]*ScrapeWork, 0, len(prev))
|
||||
for _, sc := range cfg.ScrapeConfigs {
|
||||
dstLen := len(dst)
|
||||
ok := true
|
||||
for j := range sc.AzureSDConfigs {
|
||||
sdc := &sc.AzureSDConfigs[j]
|
||||
var okLocal bool
|
||||
dst, okLocal = appendSDScrapeWork(dst, sdc, cfg.baseDir, sc.swc, "azure_sd_config")
|
||||
if ok {
|
||||
ok = okLocal
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
swsPrev := swsPrevByJob[sc.swc.jobName]
|
||||
if len(swsPrev) > 0 {
|
||||
logger.Errorf("there were errors when discovering azure targets for job %q, so preserving the previous targets", sc.swc.jobName)
|
||||
dst = append(dst[:dstLen], swsPrev...)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// getConsulSDScrapeWork returns `consul_sd_configs` ScrapeWork from cfg.
|
||||
func (cfg *Config) getConsulSDScrapeWork(prev []*ScrapeWork) []*ScrapeWork {
|
||||
swsPrevByJob := getSWSByJob(prev)
|
||||
|
|
273
lib/promscrape/discovery/azure/api.go
Normal file
273
lib/promscrape/discovery/azure/api.go
Normal file
|
@ -0,0 +1,273 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
|
||||
"github.com/VictoriaMetrics/fasthttp"
|
||||
)
|
||||
|
||||
var configMap = discoveryutils.NewConfigMap()
|
||||
|
||||
// Extract from the needed params from https://github.com/Azure/go-autorest/blob/7dd32b67be4e6c9386b9ba7b1c44a51263f05270/autorest/azure/environments.go#L61
|
||||
type cloudEnvironmentEndpoints struct {
|
||||
ActiveDirectoryEndpoint string `json:"activeDirectoryEndpoint"`
|
||||
ResourceManagerEndpoint string `json:"resourceManagerEndpoint"`
|
||||
}
|
||||
|
||||
// well-known azure cloud endpoints
|
||||
// See https://github.com/Azure/go-autorest/blob/7dd32b67be4e6c9386b9ba7b1c44a51263f05270/autorest/azure/environments.go#L34
|
||||
var cloudEnvironments = map[string]*cloudEnvironmentEndpoints{
|
||||
"AZURECHINACLOUD": {
|
||||
ActiveDirectoryEndpoint: "https://login.chinacloudapi.cn",
|
||||
ResourceManagerEndpoint: "https://management.chinacloudapi.cn",
|
||||
},
|
||||
"AZUREGERMANCLOUD": {
|
||||
ActiveDirectoryEndpoint: "https://login.microsoftonline.de",
|
||||
ResourceManagerEndpoint: "https://management.microsoftazure.de",
|
||||
},
|
||||
"AZURECLOUD": {
|
||||
ActiveDirectoryEndpoint: "https://login.microsoftonline.com",
|
||||
ResourceManagerEndpoint: "https://management.azure.com",
|
||||
},
|
||||
"AZUREPUBLICCLOUD": {
|
||||
ActiveDirectoryEndpoint: "https://login.microsoftonline.com",
|
||||
ResourceManagerEndpoint: "https://management.azure.com",
|
||||
},
|
||||
"AZUREUSGOVERNMENT": {
|
||||
ActiveDirectoryEndpoint: "https://login.microsoftonline.us",
|
||||
ResourceManagerEndpoint: "https://management.usgovcloudapi.net",
|
||||
},
|
||||
"AZUREUSGOVERNMENTCLOUD": {
|
||||
ActiveDirectoryEndpoint: "https://login.microsoftonline.us",
|
||||
ResourceManagerEndpoint: "https://management.usgovcloudapi.net",
|
||||
},
|
||||
}
|
||||
|
||||
// apiConfig contains config for API server.
|
||||
type apiConfig struct {
|
||||
c *discoveryutils.Client
|
||||
port int
|
||||
resourceGroup string
|
||||
subscriptionID string
|
||||
tenantID string
|
||||
|
||||
refreshToken refreshTokenFunc
|
||||
// tokenLock guards auth token and tokenExpireDeadline
|
||||
tokenLock sync.Mutex
|
||||
token string
|
||||
tokenExpireDeadline time.Time
|
||||
}
|
||||
|
||||
type refreshTokenFunc func() (string, time.Duration, error)
|
||||
|
||||
func getAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
|
||||
v, err := configMap.Get(sdc, func() (interface{}, error) { return newAPIConfig(sdc, baseDir) })
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v.(*apiConfig), nil
|
||||
}
|
||||
|
||||
func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
|
||||
if sdc.SubscriptionID == "" {
|
||||
return nil, fmt.Errorf("missing `subscription_id` config option")
|
||||
}
|
||||
port := sdc.Port
|
||||
if port == 0 {
|
||||
port = 80
|
||||
}
|
||||
|
||||
ac, err := sdc.HTTPClientConfig.NewConfig(baseDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse auth config: %w", err)
|
||||
}
|
||||
proxyAC, err := sdc.ProxyClientConfig.NewConfig(baseDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse proxy auth config: %w", err)
|
||||
}
|
||||
|
||||
environment := sdc.Environment
|
||||
if environment == "" {
|
||||
environment = "AZURECLOUD"
|
||||
}
|
||||
env, err := getCloudEnvByName(environment)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read configs for `environment: %q`: %w", environment, err)
|
||||
}
|
||||
|
||||
refreshToken, err := getRefreshTokenFunc(sdc, ac, proxyAC, env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, err := discoveryutils.NewClient(env.ResourceManagerEndpoint, ac, sdc.ProxyURL, proxyAC)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create client for %q: %w", env.ResourceManagerEndpoint, err)
|
||||
}
|
||||
cfg := &apiConfig{
|
||||
c: c,
|
||||
port: port,
|
||||
resourceGroup: sdc.ResourceGroup,
|
||||
subscriptionID: sdc.SubscriptionID,
|
||||
tenantID: sdc.TenantID,
|
||||
|
||||
refreshToken: refreshToken,
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func getCloudEnvByName(name string) (*cloudEnvironmentEndpoints, error) {
|
||||
name = strings.ToUpper(name)
|
||||
// Special case, azure cloud k8s cluster, read content from file.
|
||||
// See https://github.com/Azure/go-autorest/blob/7dd32b67be4e6c9386b9ba7b1c44a51263f05270/autorest/azure/environments.go#L301
|
||||
if name == "AZURESTACKCLOUD" {
|
||||
return readCloudEndpointsFromFile(os.Getenv("AZURE_ENVIRONMENT_FILEPATH"))
|
||||
}
|
||||
env := cloudEnvironments[name]
|
||||
if env == nil {
|
||||
var supportedEnvs []string
|
||||
for envName := range cloudEnvironments {
|
||||
supportedEnvs = append(supportedEnvs, envName)
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported `environment: %q`; supported values: %s", name, strings.Join(supportedEnvs, ","))
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
func readCloudEndpointsFromFile(filePath string) (*cloudEnvironmentEndpoints, error) {
|
||||
data, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot file %q: %w", filePath, err)
|
||||
}
|
||||
var cee cloudEnvironmentEndpoints
|
||||
if err := json.Unmarshal(data, &cee); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse cloud environment endpoints from file %q: %w", filePath, err)
|
||||
}
|
||||
return &cee, nil
|
||||
}
|
||||
|
||||
func getRefreshTokenFunc(sdc *SDConfig, ac, proxyAC *promauth.Config, env *cloudEnvironmentEndpoints) (refreshTokenFunc, error) {
|
||||
var tokenEndpoint, tokenAPIPath string
|
||||
var modifyRequest func(request *fasthttp.Request)
|
||||
authenticationMethod := sdc.AuthenticationMethod
|
||||
if authenticationMethod == "" {
|
||||
authenticationMethod = "OAuth"
|
||||
}
|
||||
switch strings.ToLower(authenticationMethod) {
|
||||
case "oauth":
|
||||
if sdc.TenantID == "" {
|
||||
return nil, fmt.Errorf("missing `tenant_id` config option for `authentication_method: Oauth`")
|
||||
}
|
||||
if sdc.ClientID == "" {
|
||||
return nil, fmt.Errorf("missing `client_id` config option for `authentication_method: OAuth`")
|
||||
}
|
||||
if sdc.ClientSecret.String() == "" {
|
||||
return nil, fmt.Errorf("missing `client_secrect` config option for `authentication_method: OAuth`")
|
||||
}
|
||||
q := url.Values{
|
||||
"grant_type": []string{"client_credentials"},
|
||||
"client_id": []string{sdc.ClientID},
|
||||
"client_secret": []string{sdc.ClientSecret.String()},
|
||||
"resource": []string{env.ResourceManagerEndpoint},
|
||||
}
|
||||
authParams := q.Encode()
|
||||
tokenAPIPath = "/" + sdc.TenantID + "/oauth2/token"
|
||||
tokenEndpoint = env.ActiveDirectoryEndpoint
|
||||
modifyRequest = func(request *fasthttp.Request) {
|
||||
request.SetBodyString(authParams)
|
||||
request.Header.SetMethod("POST")
|
||||
}
|
||||
case "managedidentity":
|
||||
endpoint := "http://169.254.169.254/metadata/identity/oauth2/token"
|
||||
if ep := os.Getenv("MSI_ENDPOINT"); ep != "" {
|
||||
endpoint = ep
|
||||
}
|
||||
endpointURL, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse MSI endpoint url %q: %w", endpoint, err)
|
||||
}
|
||||
q := endpointURL.Query()
|
||||
|
||||
msiSecret := os.Getenv("MSI_SECRET")
|
||||
clientIDParam := "client_id"
|
||||
apiVersion := "2018-02-01"
|
||||
if msiSecret != "" {
|
||||
clientIDParam = "clientid"
|
||||
apiVersion = "2017-09-01"
|
||||
}
|
||||
q.Set("api-version", apiVersion)
|
||||
q.Set(clientIDParam, sdc.ClientID)
|
||||
q.Set("resource", env.ResourceManagerEndpoint)
|
||||
endpointURL.RawQuery = q.Encode()
|
||||
tokenAPIPath = endpointURL.RequestURI()
|
||||
tokenEndpoint = endpointURL.Scheme + "://" + endpointURL.Host
|
||||
modifyRequest = func(request *fasthttp.Request) {
|
||||
if msiSecret != "" {
|
||||
request.Header.Set("secret", msiSecret)
|
||||
} else {
|
||||
request.Header.Set("Metadata", "true")
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported `authentication_method: %q` only `OAuth` and `ManagedIdentity` are supported", authenticationMethod)
|
||||
}
|
||||
|
||||
authClient, err := discoveryutils.NewClient(tokenEndpoint, ac, sdc.ProxyURL, proxyAC)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot build auth client: %w", err)
|
||||
}
|
||||
refreshToken := func() (string, time.Duration, error) {
|
||||
data, err := authClient.GetAPIResponseWithReqParams(tokenAPIPath, modifyRequest)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
var tr tokenResponse
|
||||
if err := json.Unmarshal(data, &tr); err != nil {
|
||||
return "", 0, fmt.Errorf("cannot parse token auth response %q: %w", string(data), err)
|
||||
}
|
||||
expiresInSeconds, err := strconv.ParseInt(tr.ExpiresIn, 10, 64)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("cannot parse expiresIn param in token auth %q: %w", tr.ExpiresIn, err)
|
||||
}
|
||||
return tr.AccessToken, time.Second * time.Duration(expiresInSeconds), nil
|
||||
}
|
||||
return refreshToken, nil
|
||||
}
|
||||
|
||||
// mustGetAuthToken returns auth token
|
||||
// in case of error, logs error and return empty token
|
||||
func (ac *apiConfig) mustGetAuthToken() string {
|
||||
ac.tokenLock.Lock()
|
||||
defer ac.tokenLock.Unlock()
|
||||
|
||||
ct := time.Now()
|
||||
if ac.tokenExpireDeadline.Sub(ct) > time.Second*30 {
|
||||
return ac.token
|
||||
}
|
||||
token, expiresDuration, err := ac.refreshToken()
|
||||
if err != nil {
|
||||
logger.Errorf("cannot refresh azure auth token: %s", err)
|
||||
return ""
|
||||
}
|
||||
ac.token = token
|
||||
ac.tokenExpireDeadline = ct.Add(expiresDuration)
|
||||
return ac.token
|
||||
}
|
||||
|
||||
// tokenResponse represent response from oauth2 azure token service
|
||||
//
|
||||
// https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-go
|
||||
type tokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn string `json:"expires_in"`
|
||||
}
|
107
lib/promscrape/discovery/azure/azure.go
Normal file
107
lib/promscrape/discovery/azure/azure.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/proxy"
|
||||
)
|
||||
|
||||
// SDCheckInterval is check interval for Azure service discovery.
|
||||
var SDCheckInterval = flag.Duration("promscrape.azureSDCheckInterval", 60*time.Second, "Interval for checking for changes in Azure. "+
|
||||
"This works only if azure_sd_configs is configured in '-promscrape.config' file. "+
|
||||
"See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#azure_sd_config for details")
|
||||
|
||||
// SDConfig represents service discovery config for Azure.
|
||||
//
|
||||
// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#azure_sd_config
|
||||
type SDConfig struct {
|
||||
Environment string `yaml:"environment,omitempty"`
|
||||
|
||||
// AuthenticationMethod can be either Oauth or ManagedIdentity.
|
||||
// See https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview
|
||||
AuthenticationMethod string `yaml:"authentication_method,omitempty"`
|
||||
|
||||
SubscriptionID string `yaml:"subscription_id"`
|
||||
TenantID string `yaml:"tenant_id,omitempty"`
|
||||
ClientID string `yaml:"client_id,omitempty"`
|
||||
ClientSecret *promauth.Secret `yaml:"client_secret,omitempty"`
|
||||
ResourceGroup string `yaml:"resource_group,omitempty"`
|
||||
|
||||
// RefreshInterval time.Duration `yaml:"refresh_interval"`
|
||||
// refresh_interval is obtained from `-promscrape.azureSDCheckInterval` command-line option.
|
||||
|
||||
Port int `yaml:"port"`
|
||||
|
||||
HTTPClientConfig promauth.HTTPClientConfig `yaml:",inline"`
|
||||
ProxyURL *proxy.URL `yaml:"proxy_url,omitempty"`
|
||||
ProxyClientConfig promauth.ProxyClientConfig `yaml:",inline"`
|
||||
}
|
||||
|
||||
// GetLabels returns Consul labels according to sdc.
|
||||
func (sdc *SDConfig) GetLabels(baseDir string) ([]map[string]string, error) {
|
||||
ac, err := getAPIConfig(sdc, baseDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get API config: %w", err)
|
||||
}
|
||||
vms, err := getVirtualMachines(ac)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return appendMachineLabels(vms, ac.port, sdc), nil
|
||||
}
|
||||
|
||||
// MustStop stops further usage for sdc.
|
||||
func (sdc *SDConfig) MustStop() {
|
||||
configMap.Delete(sdc)
|
||||
}
|
||||
|
||||
func appendMachineLabels(vms []virtualMachine, port int, sdc *SDConfig) []map[string]string {
|
||||
ms := make([]map[string]string, 0, len(vms))
|
||||
for i := range vms {
|
||||
vm := &vms[i]
|
||||
for _, ips := range vm.ipAddresses {
|
||||
if ips.privateIP == "" {
|
||||
continue
|
||||
}
|
||||
addr := discoveryutils.JoinHostPort(ips.privateIP, port)
|
||||
m := map[string]string{
|
||||
"__address__": addr,
|
||||
"__meta_azure_subscription_id": sdc.SubscriptionID,
|
||||
"__meta_azure_machine_id": vm.ID,
|
||||
"__meta_azure_machine_name": vm.Name,
|
||||
"__meta_azure_machine_location": vm.Location,
|
||||
"__meta_azure_machine_private_ip": ips.privateIP,
|
||||
}
|
||||
if sdc.TenantID != "" {
|
||||
m["__meta_azure_tenant_id"] = sdc.TenantID
|
||||
}
|
||||
// /subscriptions/SUBSCRIPTION_ID/resourceGroups/RESOURCE_GROUP/providers/PROVIDER/TYPE/NAME
|
||||
idPath := strings.Split(vm.ID, "/")
|
||||
if len(idPath) > 4 {
|
||||
m["__meta_azure_machine_resource_group"] = idPath[4]
|
||||
}
|
||||
if vm.Properties.StorageProfile.OsDisk.OsType != "" {
|
||||
m["__meta_azure_machine_os_type"] = vm.Properties.StorageProfile.OsDisk.OsType
|
||||
}
|
||||
if vm.Properties.OsProfile.ComputerName != "" {
|
||||
m["__meta_azure_machine_computer_name"] = vm.Properties.OsProfile.ComputerName
|
||||
}
|
||||
if ips.publicIP != "" {
|
||||
m["__meta_azure_machine_public_ip"] = ips.publicIP
|
||||
}
|
||||
if vm.scaleSet != "" {
|
||||
m["__meta_azure_machine_scale_set"] = vm.scaleSet
|
||||
}
|
||||
for k, v := range vm.Tags {
|
||||
m[discoveryutils.SanitizeLabelName("__meta_azure_machine_tag_"+k)] = v
|
||||
}
|
||||
ms = append(ms, m)
|
||||
}
|
||||
}
|
||||
return ms
|
||||
}
|
49
lib/promscrape/discovery/azure/azure_test.go
Normal file
49
lib/promscrape/discovery/azure/azure_test.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
|
||||
)
|
||||
|
||||
func TestAppendMachineLabels(t *testing.T) {
|
||||
f := func(name string, vms []virtualMachine, expectedLabels [][]prompbmarshal.Label) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
labelss := appendMachineLabels(vms, 80, &SDConfig{SubscriptionID: "some-id"})
|
||||
var sortedLabelss [][]prompbmarshal.Label
|
||||
for _, labels := range labelss {
|
||||
sortedLabelss = append(sortedLabelss, discoveryutils.GetSortedLabels(labels))
|
||||
}
|
||||
if !reflect.DeepEqual(sortedLabelss, expectedLabels) {
|
||||
t.Fatalf("unexpected labels:\ngot\n%v\nwant\n%v", sortedLabelss, expectedLabels)
|
||||
}
|
||||
})
|
||||
}
|
||||
f("single vm", []virtualMachine{
|
||||
{
|
||||
Name: "vm-1",
|
||||
ID: "id-2",
|
||||
Type: "Azure",
|
||||
Location: "eu-west-1",
|
||||
Properties: virtualMachineProperties{OsProfile: osProfile{ComputerName: "test-1"}, StorageProfile: storageProfile{OsDisk: osDisk{OsType: "Linux"}}},
|
||||
Tags: map[string]string{"key-1": "value-1"},
|
||||
ipAddresses: []vmIPAddress{
|
||||
{privateIP: "10.10.10.1"},
|
||||
},
|
||||
},
|
||||
}, [][]prompbmarshal.Label{
|
||||
discoveryutils.GetSortedLabels(map[string]string{
|
||||
"__address__": "10.10.10.1:80",
|
||||
"__meta_azure_machine_id": "id-2",
|
||||
"__meta_azure_subscription_id": "some-id",
|
||||
"__meta_azure_machine_os_type": "Linux",
|
||||
"__meta_azure_machine_name": "vm-1",
|
||||
"__meta_azure_machine_computer_name": "test-1",
|
||||
"__meta_azure_machine_location": "eu-west-1",
|
||||
"__meta_azure_machine_private_ip": "10.10.10.1",
|
||||
"__meta_azure_machine_tag_key_1": "value-1",
|
||||
}),
|
||||
})
|
||||
}
|
204
lib/promscrape/discovery/azure/machine.go
Normal file
204
lib/promscrape/discovery/azure/machine.go
Normal file
|
@ -0,0 +1,204 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
|
||||
"github.com/VictoriaMetrics/fasthttp"
|
||||
)
|
||||
|
||||
// virtualMachine represents an Azure virtual machine (which can also be created by a VMSS)
|
||||
type virtualMachine struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Properties virtualMachineProperties `json:"properties,omitempty"`
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
// enriched during service discovery
|
||||
scaleSet string
|
||||
ipAddresses []vmIPAddress
|
||||
}
|
||||
|
||||
type vmIPAddress struct {
|
||||
publicIP string
|
||||
privateIP string
|
||||
}
|
||||
|
||||
type virtualMachineProperties struct {
|
||||
NetworkProfile networkProfile `json:"networkProfile,omitempty"`
|
||||
OsProfile osProfile `json:"osProfile,omitempty"`
|
||||
StorageProfile storageProfile `json:"storageProfile,omitempty"`
|
||||
}
|
||||
|
||||
type storageProfile struct {
|
||||
OsDisk osDisk `json:"osDisk,omitempty"`
|
||||
}
|
||||
|
||||
type osDisk struct {
|
||||
OsType string `json:"osType,omitempty"`
|
||||
}
|
||||
|
||||
type osProfile struct {
|
||||
ComputerName string `json:"computerName,omitempty"`
|
||||
}
|
||||
type networkProfile struct {
|
||||
// NetworkInterfaces - Specifies the list of resource Ids for the network interfaces associated with the virtual machine.
|
||||
NetworkInterfaces []networkInterfaceReference `json:"networkInterfaces,omitempty"`
|
||||
}
|
||||
|
||||
type networkInterfaceReference struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
// listAPIResponse generic response from list api
|
||||
type listAPIResponse struct {
|
||||
NextLink string `json:"nextLink"`
|
||||
Value []json.RawMessage `json:"value"`
|
||||
}
|
||||
|
||||
// visitAllAPIObjects iterates over list API with pagination and applies cb for each response object
|
||||
func visitAllAPIObjects(ac *apiConfig, apiURL string, cb func(data json.RawMessage) error) error {
|
||||
nextLink := apiURL
|
||||
for nextLink != "" {
|
||||
resp, err := ac.c.GetAPIResponseWithReqParams(nextLink, func(request *fasthttp.Request) {
|
||||
request.Header.Set("Authorization", "Bearer "+ac.mustGetAuthToken())
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot execute azure api request at %s: %w", nextLink, err)
|
||||
}
|
||||
var lar listAPIResponse
|
||||
if err := json.Unmarshal(resp, &lar); err != nil {
|
||||
return fmt.Errorf("cannot parse azure api response %q obtained from %s: %w", resp, nextLink, err)
|
||||
}
|
||||
for i := range lar.Value {
|
||||
if err := cb(lar.Value[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
nextLink = lar.NextLink
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getVirtualMachines
|
||||
func getVirtualMachines(ac *apiConfig) ([]virtualMachine, error) {
|
||||
vms, err := listVMs(ac)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot list virtual machines: %w", err)
|
||||
}
|
||||
scaleSetRefs, err := listScaleSetRefs(ac)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot list scaleSets: %w", err)
|
||||
}
|
||||
ssvms, err := listScaleSetVMs(ac, scaleSetRefs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot list virtual machines for scaleSets: %w", err)
|
||||
}
|
||||
vms = append(vms, ssvms...)
|
||||
if err := enrichVirtualMachinesNetworkInterfaces(ac, vms); err != nil {
|
||||
return nil, fmt.Errorf("cannot discover network interfaces for virtual machines: %w", err)
|
||||
}
|
||||
return vms, nil
|
||||
}
|
||||
|
||||
func enrichVirtualMachinesNetworkInterfaces(ac *apiConfig, vms []virtualMachine) error {
|
||||
concurrency := cgroup.AvailableCPUs() * 10
|
||||
workCh := make(chan *virtualMachine, concurrency)
|
||||
resultCh := make(chan error, concurrency)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < concurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for vm := range workCh {
|
||||
err := enrichVMNetworkInterfaces(ac, vm)
|
||||
resultCh <- err
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := range vms {
|
||||
workCh <- &vms[i]
|
||||
}
|
||||
close(workCh)
|
||||
}()
|
||||
var firstErr error
|
||||
for range vms {
|
||||
err := <-resultCh
|
||||
if err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// See https://docs.microsoft.com/en-us/rest/api/compute/virtual-machines/list-all
|
||||
func listVMs(ac *apiConfig) ([]virtualMachine, error) {
|
||||
// https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Compute/virtualMachines?api-version=2022-03-01
|
||||
apiURL := "/subscriptions/" + ac.subscriptionID
|
||||
if ac.resourceGroup != "" {
|
||||
// special case filter by resourceGroup
|
||||
// https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines?api-version=2022-03-01
|
||||
apiURL += "/resourceGroups/" + ac.resourceGroup
|
||||
}
|
||||
apiURL += "/providers/Microsoft.Compute/virtualMachines?api-version=2022-03-01"
|
||||
var vms []virtualMachine
|
||||
err := visitAllAPIObjects(ac, apiURL, func(data json.RawMessage) error {
|
||||
var vm virtualMachine
|
||||
if err := json.Unmarshal(data, &vm); err != nil {
|
||||
return fmt.Errorf("cannot parse virtualMachine list API response %q: %w", data, err)
|
||||
}
|
||||
vms = append(vms, vm)
|
||||
return nil
|
||||
})
|
||||
return vms, err
|
||||
}
|
||||
|
||||
type scaleSet struct {
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// See https://docs.microsoft.com/en-us/rest/api/compute/virtual-machine-scale-sets/list-all
|
||||
func listScaleSetRefs(ac *apiConfig) ([]scaleSet, error) {
|
||||
// https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Compute/virtualMachineScaleSets?api-version=2022-03-01
|
||||
apiURL := "/subscriptions/" + ac.subscriptionID + "/providers/Microsoft.Compute/virtualMachineScaleSets?api-version=2022-03-01"
|
||||
var sss []scaleSet
|
||||
err := visitAllAPIObjects(ac, apiURL, func(data json.RawMessage) error {
|
||||
var ss scaleSet
|
||||
if err := json.Unmarshal(data, &ss); err != nil {
|
||||
return fmt.Errorf("cannot parse scaleSet list API response %q: %w", data, err)
|
||||
}
|
||||
sss = append(sss, ss)
|
||||
return nil
|
||||
})
|
||||
return sss, err
|
||||
}
|
||||
|
||||
// See https://docs.microsoft.com/en-us/rest/api/compute/virtual-machine-scale-set-vms/list
|
||||
func listScaleSetVMs(ac *apiConfig, sss []scaleSet) ([]virtualMachine, error) {
|
||||
// https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachineScaleSets/{virtualMachineScaleSetName}/virtualMachines?api-version=2022-03-01
|
||||
var vms []virtualMachine
|
||||
for _, ss := range sss {
|
||||
apiURI := ss.ID + "/virtualMachines?api-version=2022-03-01"
|
||||
err := visitAllAPIObjects(ac, apiURI, func(data json.RawMessage) error {
|
||||
var vm virtualMachine
|
||||
if err := json.Unmarshal(data, &vm); err != nil {
|
||||
return fmt.Errorf("cannot parse virtualMachine list API response %q: %w", data, err)
|
||||
}
|
||||
vm.scaleSet = ss.Name
|
||||
vms = append(vms, vm)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return vms, nil
|
||||
}
|
376
lib/promscrape/discovery/azure/machine_test.go
Normal file
376
lib/promscrape/discovery/azure/machine_test.go
Normal file
|
@ -0,0 +1,376 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
|
||||
)
|
||||
|
||||
func TestGetVirtualMachinesSuccess(t *testing.T) {
|
||||
prettifyVMs := func(src []virtualMachine) string {
|
||||
var sb strings.Builder
|
||||
for idx, vm := range src {
|
||||
fmt.Fprintf(&sb, `idx: %d, vm: Name: %q, ID: %q, Location: %q, Type: %q, ComputerName: %q, OsType: %q, scaleSet: %q`,
|
||||
idx, vm.Name, vm.ID, vm.Location, vm.Type, vm.Properties.OsProfile.ComputerName, vm.Properties.StorageProfile.OsDisk.OsType, vm.scaleSet)
|
||||
if vm.Tags != nil {
|
||||
fmt.Fprint(&sb, " vmtags: ")
|
||||
}
|
||||
for tagK, tagV := range vm.Tags {
|
||||
fmt.Fprintf(&sb, `%q: %q, `, tagK, tagV)
|
||||
}
|
||||
if len(vm.Properties.NetworkProfile.NetworkInterfaces) > 0 {
|
||||
fmt.Fprint(&sb, " network ints: ")
|
||||
}
|
||||
for idx, nic := range vm.Properties.NetworkProfile.NetworkInterfaces {
|
||||
fmt.Fprintf(&sb, " idx %d, ID: %q", idx, nic.ID)
|
||||
}
|
||||
if len(vm.ipAddresses) > 0 {
|
||||
fmt.Fprint(&sb, " ip addresses: ")
|
||||
}
|
||||
for idx, ip := range vm.ipAddresses {
|
||||
fmt.Fprintf(&sb, "idx: %d, PrivateIP: %q, PublicIP: %q", idx, ip.privateIP, ip.publicIP)
|
||||
}
|
||||
fmt.Fprintf(&sb, "\n")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
f := func(name string, expectedVMs []virtualMachine, apiResponses [4]string) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
// list vms response
|
||||
case strings.Contains(r.URL.Path, "/providers/Microsoft.Compute/virtualMachines"):
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, apiResponses[0])
|
||||
// list scaleSets response
|
||||
case strings.Contains(r.URL.RequestURI(), "/providers/Microsoft.Compute/virtualMachineScaleSets?api-version=2022-03-01"):
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, apiResponses[1])
|
||||
// list scalesets vms response
|
||||
case strings.Contains(r.URL.Path, "/providers/Microsoft.Compute/virtualMachineScaleSets/{virtualMachineScaleSetName}/virtualMach"):
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, apiResponses[2])
|
||||
// nic response
|
||||
case strings.Contains(r.URL.Path, "/networkInterfaces/"):
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, apiResponses[3])
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintf(w, "API path not found: %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer testServer.Close()
|
||||
c, err := discoveryutils.NewClient(testServer.URL, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error at client create: %s", err)
|
||||
}
|
||||
ac := &apiConfig{
|
||||
c: c,
|
||||
subscriptionID: "some-id",
|
||||
refreshToken: func() (string, time.Duration, error) {
|
||||
return "auth-token", 0, nil
|
||||
},
|
||||
}
|
||||
gotVMs, err := getVirtualMachines(ac)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(gotVMs, expectedVMs) {
|
||||
t.Fatalf("unexpected test result\ngot:\n%s\nwant:\n%s", prettifyVMs(gotVMs), prettifyVMs(expectedVMs))
|
||||
}
|
||||
})
|
||||
}
|
||||
f("discover single vm", []virtualMachine{
|
||||
{
|
||||
Name: "{virtualMachineName}", ID: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{virtualMachineName}",
|
||||
Location: "eastus", Type: "Microsoft.Compute/virtualMachines",
|
||||
Properties: virtualMachineProperties{
|
||||
NetworkProfile: networkProfile{NetworkInterfaces: []networkInterfaceReference{{ID: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/networkInterfaces/{networkInterfaceName}"}}},
|
||||
OsProfile: osProfile{ComputerName: "Test"},
|
||||
StorageProfile: storageProfile{OsDisk: osDisk{OsType: "Windows"}},
|
||||
},
|
||||
ipAddresses: []vmIPAddress{
|
||||
{publicIP: "20.30.40.50", privateIP: "172.20.2.4"},
|
||||
},
|
||||
Tags: map[string]string{},
|
||||
},
|
||||
}, [4]string{
|
||||
`
|
||||
{
|
||||
"value": [
|
||||
{ "id": "/some-vm/id",
|
||||
"properties": {
|
||||
"vmId": "{vmId}",
|
||||
"storageProfile": {
|
||||
"imageReference": {
|
||||
"publisher": "MicrosoftWindowsServer",
|
||||
"offer": "WindowsServer",
|
||||
"sku": "2012-R2-Datacenter",
|
||||
"version": "4.127.20170406",
|
||||
"exactVersion": "aaaaaaaaaaaaa",
|
||||
"sharedGalleryImageId": "aaaaaaaaaaaaaaa",
|
||||
"communityGalleryImageId": "aaaa",
|
||||
"id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
},
|
||||
"osDisk": {
|
||||
"osType": "Windows",
|
||||
"name": "test",
|
||||
"createOption": "FromImage",
|
||||
"vhd": {
|
||||
"uri": "https://{storageAccountName}.blob.core.windows.net/{containerName}/{vhdName}.vhd"
|
||||
},
|
||||
"caching": "None",
|
||||
"diskSizeGB": 127,
|
||||
"encryptionSettings": {
|
||||
"diskEncryptionKey": {
|
||||
"secretUrl": "aaaaaaaaa",
|
||||
"sourceVault": {
|
||||
"id": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/availabilitySets/{availabilitySetName}"
|
||||
}
|
||||
},
|
||||
"keyEncryptionKey": {
|
||||
"keyUrl": "aaaaaaaaaaaaa",
|
||||
"sourceVault": {
|
||||
"id": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/availabilitySets/{availabilitySetName}"
|
||||
}
|
||||
},
|
||||
"enabled": true
|
||||
},
|
||||
"image": {
|
||||
"uri": "https://{storageAccountName}.blob.core.windows.net/{containerName}/{vhdName}.vhd"
|
||||
},
|
||||
"writeAcceleratorEnabled": true,
|
||||
"diffDiskSettings": {
|
||||
"option": "Local",
|
||||
"placement": "CacheDisk"
|
||||
},
|
||||
"managedDisk": {
|
||||
"storageAccountType": "Standard_LRS",
|
||||
"diskEncryptionSet": {
|
||||
"id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
},
|
||||
"securityProfile": {
|
||||
"securityEncryptionType": "VMGuestStateOnly",
|
||||
"diskEncryptionSet": {
|
||||
"id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
}
|
||||
},
|
||||
"id": "/subscriptions/{subscription-id}/resourceGroups/myResourceGroup/providers/Microsoft.Compute/disks/testingexcludedisk_OsDisk_1_74cdaedcea50483d9833c96adefa100f"
|
||||
},
|
||||
"deleteOption": "Delete"
|
||||
},
|
||||
"dataDisks": []
|
||||
},
|
||||
"osProfile": {
|
||||
"computerName": "Test",
|
||||
"adminUsername": "Foo12"
|
||||
},
|
||||
"networkProfile": {
|
||||
"networkInterfaces": [
|
||||
{
|
||||
"id": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/networkInterfaces/{networkInterfaceName}",
|
||||
"properties": {
|
||||
"primary": true,
|
||||
"deleteOption": "Delete"
|
||||
}
|
||||
}
|
||||
],
|
||||
"networkApiVersion": "2020-11-01"
|
||||
}
|
||||
},
|
||||
"type": "Microsoft.Compute/virtualMachines",
|
||||
"location": "eastus",
|
||||
"tags": {},
|
||||
"id": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{virtualMachineName}",
|
||||
"name": "{virtualMachineName}"
|
||||
}
|
||||
],
|
||||
"nextLink": ""
|
||||
}`,
|
||||
`{}`,
|
||||
`{}`,
|
||||
`{
|
||||
"name": "test-nic",
|
||||
"properties": {
|
||||
"ipConfigurations": [
|
||||
{
|
||||
"name": "ipconfig1",
|
||||
"properties": {
|
||||
"privateIPAddress": "172.20.2.4",
|
||||
"publicIPAddress": {
|
||||
"properties": {
|
||||
"ipAddress": "20.30.40.50"
|
||||
}
|
||||
},
|
||||
"primary": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"primary": true
|
||||
},
|
||||
"type": "Microsoft.Network/networkInterfaces"
|
||||
}`,
|
||||
})
|
||||
|
||||
f("discover vm with scaleSet", []virtualMachine{
|
||||
{
|
||||
Name: "{vmss-vm-name}", ID: "/subscriptions/{subscription-id}/resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachineScaleSets/{vmss-name}/virtualMachines/0",
|
||||
Location: "westus", Type: "Microsoft.Compute/virtualMachines",
|
||||
Properties: virtualMachineProperties{
|
||||
NetworkProfile: networkProfile{NetworkInterfaces: []networkInterfaceReference{
|
||||
{ID: "/subscriptions/{subscription-id}/resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachineScaleSets/{vmss-name}/virtualMachines/0/networkInterfaces/vmsstestnetconfig5415"},
|
||||
{ID: "/subscriptions/{subscription-id}/resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachineScaleSets/{vmss-name}/virtualMachines/0/networkInterfaces/vmsstestnetconfig5415"},
|
||||
}},
|
||||
OsProfile: osProfile{ComputerName: "test000000"},
|
||||
StorageProfile: storageProfile{OsDisk: osDisk{OsType: "Windows"}},
|
||||
},
|
||||
scaleSet: "{virtualMachineScaleSetName}",
|
||||
ipAddresses: []vmIPAddress{
|
||||
{publicIP: "20.30.40.50", privateIP: "172.20.2.4"},
|
||||
{publicIP: "20.30.40.50", privateIP: "172.20.2.4"},
|
||||
},
|
||||
Tags: map[string]string{},
|
||||
},
|
||||
{
|
||||
Name: "{vmss-vm-name}", ID: "/subscriptions/{subscription-id}/resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachineScaleSets/{vmss-name}/virtualMachines/15",
|
||||
Location: "westp", Type: "Microsoft.Compute/virtualMachines",
|
||||
Properties: virtualMachineProperties{
|
||||
NetworkProfile: networkProfile{NetworkInterfaces: []networkInterfaceReference{
|
||||
{ID: "/subscriptions/{subscription-id}/resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachineScaleSets/{vmss-name}/virtualMachines/0/networkInterfaces/vmsstestnetconfig5415"},
|
||||
}},
|
||||
OsProfile: osProfile{ComputerName: "test-15"},
|
||||
StorageProfile: storageProfile{OsDisk: osDisk{OsType: "Linux"}},
|
||||
},
|
||||
scaleSet: "{virtualMachineScaleSetName}",
|
||||
ipAddresses: []vmIPAddress{
|
||||
{publicIP: "20.30.40.50", privateIP: "172.20.2.4"},
|
||||
},
|
||||
Tags: map[string]string{},
|
||||
},
|
||||
}, [4]string{
|
||||
`{}`,
|
||||
`{
|
||||
"value": [
|
||||
{
|
||||
"sku": {
|
||||
"tier": "Standard",
|
||||
"capacity": 3,
|
||||
"name": "Standard_D1_v2"
|
||||
},
|
||||
"location": "westus",
|
||||
"properties": { },
|
||||
"id": "/subscriptions/{subscription-id}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachineScaleSets/{virtualMachineScaleSetName}",
|
||||
"name": "{virtualMachineScaleSetName}",
|
||||
"type": "Microsoft.Compute/virtualMachineScaleSets",
|
||||
"tags": {
|
||||
"key8425": "aaa"
|
||||
}
|
||||
}
|
||||
],
|
||||
"nextLink": ""
|
||||
}`,
|
||||
`
|
||||
{
|
||||
"value": [
|
||||
{
|
||||
"name": "{vmss-vm-name}",
|
||||
"id": "/subscriptions/{subscription-id}/resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachineScaleSets/{vmss-name}/virtualMachines/0",
|
||||
"type": "Microsoft.Compute/virtualMachines",
|
||||
"location": "westus",
|
||||
"tags": {},
|
||||
"properties": {
|
||||
"storageProfile": {
|
||||
"osDisk": {
|
||||
"osType": "Windows"
|
||||
}
|
||||
},
|
||||
"osProfile": {
|
||||
"computerName": "test000000"
|
||||
},
|
||||
"networkProfile": {
|
||||
"networkInterfaces": [
|
||||
{
|
||||
"id": "/subscriptions/{subscription-id}/resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachineScaleSets/{vmss-name}/virtualMachines/0/networkInterfaces/vmsstestnetconfig5415",
|
||||
"properties": {
|
||||
"primary": true,
|
||||
"deleteOption": "Delete"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "/subscriptions/{subscription-id}/resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachineScaleSets/{vmss-name}/virtualMachines/0/networkInterfaces/vmsstestnetconfig5415",
|
||||
"properties": {
|
||||
"primary": true,
|
||||
"deleteOption": "Delete"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"licenseType": "aaaaaaaaaa",
|
||||
"protectionPolicy": {
|
||||
"protectFromScaleIn": true,
|
||||
"protectFromScaleSetActions": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "{vmss-vm-name}",
|
||||
"id": "/subscriptions/{subscription-id}/resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachineScaleSets/{vmss-name}/virtualMachines/15",
|
||||
"type": "Microsoft.Compute/virtualMachines",
|
||||
"location": "westp",
|
||||
"tags": {},
|
||||
"properties": {
|
||||
"storageProfile": {
|
||||
"osDisk": {
|
||||
"osType": "Linux"
|
||||
}
|
||||
},
|
||||
"osProfile": {
|
||||
"computerName": "test-15"
|
||||
},
|
||||
"networkProfile": {
|
||||
"networkInterfaces": [
|
||||
{
|
||||
"id": "/subscriptions/{subscription-id}/resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachineScaleSets/{vmss-name}/virtualMachines/0/networkInterfaces/vmsstestnetconfig5415",
|
||||
"properties": {
|
||||
"primary": true,
|
||||
"deleteOption": "Delete"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"licenseType": "aaaaaaaaaa"
|
||||
}
|
||||
}
|
||||
|
||||
],
|
||||
"nextLink": ""
|
||||
}`,
|
||||
`{
|
||||
"name": "test-nic",
|
||||
"properties": {
|
||||
"ipConfigurations": [
|
||||
{
|
||||
"name": "ipconfig1",
|
||||
"properties": {
|
||||
"privateIPAddress": "172.20.2.4",
|
||||
"publicIPAddress": {
|
||||
"properties": {
|
||||
"ipAddress": "20.30.40.50"
|
||||
}
|
||||
},
|
||||
"primary": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"primary": true
|
||||
},
|
||||
"type": "Microsoft.Network/networkInterfaces"
|
||||
}`,
|
||||
})
|
||||
}
|
80
lib/promscrape/discovery/azure/nic.go
Normal file
80
lib/promscrape/discovery/azure/nic.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/VictoriaMetrics/fasthttp"
|
||||
)
|
||||
|
||||
// networkInterface a network interface in a resource group.
|
||||
type networkInterface struct {
|
||||
Properties networkProperties `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
type networkProperties struct {
|
||||
// Primary - Gets whether this is a primary network interface on a virtual machine.
|
||||
Primary bool `json:"primary,omitempty"`
|
||||
IPConfigurations []ipConfiguration `json:"ipConfigurations,omitempty"`
|
||||
}
|
||||
|
||||
type ipConfiguration struct {
|
||||
Properties ipProperties `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
type ipProperties struct {
|
||||
PublicIPAddress publicIPAddress `json:"publicIPAddress,omitempty"`
|
||||
PrivateIPAddress string `json:"privateIPAddress,omitempty"`
|
||||
}
|
||||
|
||||
type publicIPAddress struct {
|
||||
Properties publicIPProperties `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
type publicIPProperties struct {
|
||||
IPAddress string `json:"ipAddress,omitempty"`
|
||||
}
|
||||
|
||||
func enrichVMNetworkInterfaces(ac *apiConfig, vm *virtualMachine) error {
|
||||
for _, nicRef := range vm.Properties.NetworkProfile.NetworkInterfaces {
|
||||
isScaleSetVM := vm.scaleSet != ""
|
||||
nic, err := getNIC(ac, nicRef.ID, isScaleSetVM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// only primary interface is relevant for us
|
||||
// mimic Prometheus logic
|
||||
if nic.Properties.Primary {
|
||||
for _, ipCfg := range nic.Properties.IPConfigurations {
|
||||
vm.ipAddresses = append(vm.ipAddresses, vmIPAddress{
|
||||
publicIP: ipCfg.Properties.PublicIPAddress.Properties.IPAddress,
|
||||
privateIP: ipCfg.Properties.PrivateIPAddress,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// See https://docs.microsoft.com/en-us/rest/api/virtualnetwork/network-interfaces/get
|
||||
func getNIC(ac *apiConfig, id string, isScaleSetVM bool) (*networkInterface, error) {
|
||||
// https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/networkInterfaces/{networkInterfaceName}?api-version=2021-08-01
|
||||
apiQueryParams := "api-version=2021-08-01&$expand=ipConfigurations/publicIPAddress"
|
||||
// special case for VMs managed by ScaleSet
|
||||
// it's not documented at API docs.
|
||||
if isScaleSetVM {
|
||||
apiQueryParams = "api-version=2021-03-01&$expand=ipConfigurations/publicIPAddress"
|
||||
}
|
||||
apiURL := id + "?" + apiQueryParams
|
||||
resp, err := ac.c.GetAPIResponseWithReqParams(apiURL, func(request *fasthttp.Request) {
|
||||
request.Header.Set("Authorization", "Bearer "+ac.mustGetAuthToken())
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute api request at %s :%w", apiURL, err)
|
||||
}
|
||||
var nic networkInterface
|
||||
if err := json.Unmarshal(resp, &nic); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse network-interface api response %q: %w", resp, err)
|
||||
}
|
||||
return &nic, nil
|
||||
}
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/azure"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/consul"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/digitalocean"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/dns"
|
||||
|
@ -110,6 +111,7 @@ func runScraper(configFile string, pushData func(wr *prompbmarshal.WriteRequest)
|
|||
cfg.mustStart()
|
||||
|
||||
scs := newScrapeConfigs(pushData, globalStopCh)
|
||||
scs.add("azure_sd_configs", *azure.SDCheckInterval, func(cfg *Config, swsPrev []*ScrapeWork) []*ScrapeWork { return cfg.getAzureSDScrapeWork(swsPrev) })
|
||||
scs.add("consul_sd_configs", *consul.SDCheckInterval, func(cfg *Config, swsPrev []*ScrapeWork) []*ScrapeWork { return cfg.getConsulSDScrapeWork(swsPrev) })
|
||||
scs.add("digitalocean_sd_configs", *digitalocean.SDCheckInterval, func(cfg *Config, swsPrev []*ScrapeWork) []*ScrapeWork { return cfg.getDigitalOceanDScrapeWork(swsPrev) })
|
||||
scs.add("dns_sd_configs", *dns.SDCheckInterval, func(cfg *Config, swsPrev []*ScrapeWork) []*ScrapeWork { return cfg.getDNSSDScrapeWork(swsPrev) })
|
||||
|
|
Loading…
Reference in a new issue