This is an automated email from the ASF dual-hosted git repository.
phanikumv pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 32c88a2906 Add exclude/include events filters to audit log (#38506)
32c88a2906 is described below
commit 32c88a29060ee9d1a083d5ea1d970a99f3d5ddeb
Author: Brent Bovenzi <[email protected]>
AuthorDate: Tue Apr 2 04:48:59 2024 -0400
Add exclude/include events filters to audit log (#38506)
Co-authored-by: Wei Lee <[email protected]>
---
airflow/www/extensions/init_jinja_globals.py | 2 +
airflow/www/static/js/api/useEventLogs.tsx | 12 ++++
airflow/www/static/js/dag/details/AuditLog.tsx | 83 ++++++++++++++++++++++++--
airflow/www/static/js/datasets/SearchBar.tsx | 77 ++++++++++++------------
airflow/www/templates/airflow/dag.html | 2 +
airflow/www/views.py | 5 ++
tests/www/views/test_views.py | 2 +
7 files changed, 142 insertions(+), 41 deletions(-)
diff --git a/airflow/www/extensions/init_jinja_globals.py
b/airflow/www/extensions/init_jinja_globals.py
index e13a46be7b..a64212ef7b 100644
--- a/airflow/www/extensions/init_jinja_globals.py
+++ b/airflow/www/extensions/init_jinja_globals.py
@@ -76,6 +76,8 @@ def init_jinja_globals(app):
"k8s_or_k8scelery_executor": IS_K8S_OR_K8SCELERY_EXECUTOR,
"rest_api_enabled": False,
"config_test_connection": conf.get("core", "test_connection",
fallback="Disabled"),
+ "included_events_raw": conf.get("webserver",
"audit_view_included_events", fallback=""),
+ "excluded_events_raw": conf.get("webserver",
"audit_view_excluded_events", fallback=""),
}
# Extra global specific to auth manager
diff --git a/airflow/www/static/js/api/useEventLogs.tsx
b/airflow/www/static/js/api/useEventLogs.tsx
index 3a04eac393..4b5cc92ce5 100644
--- a/airflow/www/static/js/api/useEventLogs.tsx
+++ b/airflow/www/static/js/api/useEventLogs.tsx
@@ -34,6 +34,8 @@ export default function useEventLogs({
after,
before,
owner,
+ includedEvents,
+ excludedEvents,
}: API.GetEventLogsVariables) {
const { isRefreshOn } = useAutoRefresh();
return useQuery(
@@ -48,10 +50,18 @@ export default function useEventLogs({
after,
before,
owner,
+ excludedEvents,
+ includedEvents,
],
() => {
const eventsLogUrl = getMetaValue("event_logs_api");
const orderParam = orderBy ? { order_by: orderBy } : {};
+ const excludedParam = excludedEvents
+ ? { excluded_events: excludedEvents }
+ : {};
+ const includedParam = includedEvents
+ ? { included_events: includedEvents }
+ : {};
return axios.get<AxiosResponse, API.EventLogCollection>(eventsLogUrl, {
params: {
offset,
@@ -60,6 +70,8 @@ export default function useEventLogs({
...{ task_id: taskId },
...{ run_id: runId },
...orderParam,
+ ...excludedParam,
+ ...includedParam,
after,
before,
},
diff --git a/airflow/www/static/js/dag/details/AuditLog.tsx
b/airflow/www/static/js/dag/details/AuditLog.tsx
index 11f5447a98..e30fbbe68e 100644
--- a/airflow/www/static/js/dag/details/AuditLog.tsx
+++ b/airflow/www/static/js/dag/details/AuditLog.tsx
@@ -19,7 +19,7 @@
/* global moment */
-import React, { useMemo, useRef } from "react";
+import React, { useMemo, useRef, useState } from "react";
import {
Box,
Flex,
@@ -27,12 +27,20 @@ import {
FormHelperText,
FormLabel,
Input,
- HStack,
+ SimpleGrid,
Button,
} from "@chakra-ui/react";
import { createColumnHelper } from "@tanstack/react-table";
import { snakeCase } from "lodash";
+import {
+ OptionBase,
+ useChakraSelectProps,
+ CreatableSelect,
+ GroupBase,
+ ChakraStylesConfig,
+} from "chakra-react-select";
+
import { useEventLogs } from "src/api";
import { getMetaValue, useOffsetTop } from "src/utils";
import type { DagRun } from "src/types";
@@ -43,11 +51,19 @@ import { useTableURLState } from
"src/components/NewTable/useTableUrlState";
import { CodeCell, TimeCell } from "src/components/NewTable/NewCells";
import { MdRefresh } from "react-icons/md";
+const configExcludedEvents = getMetaValue("excluded_audit_log_events");
+const configIncludedEvents = getMetaValue("included_audit_log_events");
+
interface Props {
taskId?: string;
run?: DagRun;
}
+interface Option extends OptionBase {
+ label: string;
+ value: string;
+}
+
const dagId = getMetaValue("dag_id") || undefined;
const columnHelper = createColumnHelper<EventLog>();
@@ -58,6 +74,12 @@ const AuditLog = ({ taskId, run }: Props) => {
const { tableURLState, setTableURLState } = useTableURLState({
sorting: [{ id: "when", desc: true }],
});
+ const [includedEvents, setIncludedEvents] = useState(
+ configIncludedEvents.length ? configIncludedEvents.split(",") : []
+ );
+ const [excludedEvents, setExcludedEvents] = useState(
+ configExcludedEvents.length ? configExcludedEvents.split(",") : []
+ );
const sort = tableURLState.sorting[0];
const orderBy = sort ? `${sort.desc ? "-" : ""}${snakeCase(sort.id)}` : "";
@@ -72,6 +94,8 @@ const AuditLog = ({ taskId, run }: Props) => {
limit: tableURLState.pagination.pageSize,
offset:
tableURLState.pagination.pageIndex * tableURLState.pagination.pageSize,
+ includedEvents: includedEvents ? includedEvents.join(",") : undefined,
+ excludedEvents: excludedEvents ? excludedEvents.join(",") : undefined,
});
const columns = useMemo(() => {
@@ -116,6 +140,49 @@ const AuditLog = ({ taskId, run }: Props) => {
const memoData = useMemo(() => data?.eventLogs, [data?.eventLogs]);
+ const chakraStyles: ChakraStylesConfig<Option, true, GroupBase<Option>> = {
+ dropdownIndicator: (provided) => ({
+ ...provided,
+ display: "none",
+ }),
+ indicatorSeparator: (provided) => ({
+ ...provided,
+ display: "none",
+ }),
+ menuList: (provided) => ({
+ ...provided,
+ py: 0,
+ }),
+ };
+
+ const excludedEventsSelectProps = useChakraSelectProps<Option, true>({
+ isMulti: true,
+ tagVariant: "solid",
+ value: excludedEvents.map((e) => ({
+ label: e,
+ value: e,
+ })),
+ onChange: (options) => {
+ setExcludedEvents((options || []).map(({ value }) => value));
+ },
+ placeholder: "Type to filter an event",
+ chakraStyles,
+ });
+
+ const includedEventsSelectProps = useChakraSelectProps<Option, true>({
+ isMulti: true,
+ tagVariant: "solid",
+ value: includedEvents.map((e) => ({
+ label: e,
+ value: e,
+ })),
+ onChange: (options) => {
+ setIncludedEvents((options || []).map(({ value }) => value));
+ },
+ placeholder: "Type to filter an event",
+ chakraStyles,
+ });
+
return (
<Box
height="100%"
@@ -137,7 +204,7 @@ const AuditLog = ({ taskId, run }: Props) => {
View full cluster Audit Log
</LinkButton>
</Flex>
- <HStack spacing={2} alignItems="flex-start">
+ <SimpleGrid columns={4} columnGap={2}>
<FormControl>
<FormLabel>Show Logs After</FormLabel>
<Input
@@ -178,7 +245,15 @@ const AuditLog = ({ taskId, run }: Props) => {
<Input placeholder={taskId} isDisabled />
<FormHelperText />
</FormControl>
- </HStack>
+ <FormControl>
+ <FormLabel>Events to exclude</FormLabel>
+ <CreatableSelect {...excludedEventsSelectProps} />
+ </FormControl>
+ <FormControl>
+ <FormLabel>Events to include</FormLabel>
+ <CreatableSelect {...includedEventsSelectProps} />
+ </FormControl>
+ </SimpleGrid>
<NewTable
key={`${taskId}-${run?.runId}`}
data={memoData || []}
diff --git a/airflow/www/static/js/datasets/SearchBar.tsx
b/airflow/www/static/js/datasets/SearchBar.tsx
index 094bb20985..4702035bf2 100644
--- a/airflow/www/static/js/datasets/SearchBar.tsx
+++ b/airflow/www/static/js/datasets/SearchBar.tsx
@@ -18,10 +18,9 @@
*/
import React from "react";
-import { Size, useChakraSelectProps } from "chakra-react-select";
+import { Select, SingleValue, useChakraSelectProps } from
"chakra-react-select";
import type { DatasetDependencies } from "src/api/useDatasetDependencies";
-import MultiSelect from "src/components/MultiSelect";
interface Props {
datasetDependencies?: DatasetDependencies;
@@ -50,17 +49,47 @@ const SearchBar = ({
datasetOptions.push({ value: node.id, label: node.value.label });
});
- const inputStyles: { backgroundColor: string; size: Size } = {
- backgroundColor: "white",
- size: "lg",
+ const onSelect = (option: SingleValue<Option>) => {
+ let type = "";
+ if (option) {
+ if (option.value.startsWith("dataset:")) type = "dataset";
+ else if (option.value.startsWith("dag:")) type = "dag";
+ if (type) onSelectNode(option.label, type);
+ }
};
- const selectStyles = useChakraSelectProps({
- ...inputStyles,
- tagVariant: "solid",
- // hideSelectedOptions: false,
- // isClearable: false,
+
+ let option;
+ if (selectedUri) {
+ option = { label: selectedUri, value: `dataset:${selectedUri}` };
+ } else if (selectedDagId) {
+ option = { label: selectedDagId, value: `dag:${selectedDagId}` };
+ }
+
+ const searchProps = useChakraSelectProps<Option, false>({
selectedOptionStyle: "check",
+ isDisabled: !datasetDependencies,
+ value: option,
+ onChange: onSelect,
+ options: [
+ { label: "DAGs", options: dagOptions },
+ { label: "Datasets", options: datasetOptions },
+ ],
+ placeholder: "Search by DAG ID or Dataset URI",
chakraStyles: {
+ dropdownIndicator: (provided) => ({
+ ...provided,
+ bg: "transparent",
+ px: 2,
+ cursor: "inherit",
+ }),
+ indicatorSeparator: (provided) => ({
+ ...provided,
+ display: "none",
+ }),
+ menuList: (provided) => ({
+ ...provided,
+ py: 0,
+ }),
container: (p) => ({
...p,
width: "100%",
@@ -93,33 +122,7 @@ const SearchBar = ({
},
});
- const onSelect = ({ label, value }: Option) => {
- let type = "";
- if (value.startsWith("dataset:")) type = "dataset";
- else if (value.startsWith("dag:")) type = "dag";
- if (type) onSelectNode(label, type);
- };
-
- let option;
- if (selectedUri) {
- option = { label: selectedUri, value: `dataset:${selectedUri}` };
- } else if (selectedDagId) {
- option = { label: selectedDagId, value: `dag:${selectedDagId}` };
- }
-
- return (
- <MultiSelect
- {...selectStyles}
- isDisabled={!datasetDependencies}
- value={option}
- onChange={(e) => onSelect(e as Option)}
- options={[
- { label: "DAGs", options: dagOptions },
- { label: "Datasets", options: datasetOptions },
- ]}
- placeholder="Search by DAG ID or Dataset URI"
- />
- );
+ return <Select {...searchProps} />;
};
export default SearchBar;
diff --git a/airflow/www/templates/airflow/dag.html
b/airflow/www/templates/airflow/dag.html
index 9cb4d430a6..56bfc27070 100644
--- a/airflow/www/templates/airflow/dag.html
+++ b/airflow/www/templates/airflow/dag.html
@@ -89,6 +89,8 @@
<meta name="is_paused" content="{{ dag_is_paused }}">
<meta name="csrf_token" content="{{ csrf_token() }}">
<meta name="k8s_or_k8scelery_executor" content="{{ k8s_or_k8scelery_executor
}}">
+ <meta name="excluded_audit_log_events" content="{{ excluded_events_raw }}">
+ <meta name="included_audit_log_events" content="{{ included_events_raw }}">
{% if dag_model is defined and dag_model.next_dagrun_create_after is defined
and dag_model.next_dagrun_create_after is not none %}
<meta name="next_dagrun_create_after" content="{{
dag_model.next_dagrun_create_after }}">
<meta name="next_dagrun_data_interval_start" content="{{
dag_model.next_dagrun_data_interval_start }}">
diff --git a/airflow/www/views.py b/airflow/www/views.py
index a9b3d65ca1..8a5ea38270 100644
--- a/airflow/www/views.py
+++ b/airflow/www/views.py
@@ -2796,6 +2796,9 @@ class Airflow(AirflowBaseView):
wwwutils.check_import_errors(dag.fileloc, session)
wwwutils.check_dag_warnings(dag.dag_id, session)
+ included_events_raw = conf.get("webserver",
"audit_view_included_events", fallback="")
+ excluded_events_raw = conf.get("webserver",
"audit_view_excluded_events", fallback="")
+
root = request.args.get("root")
if root:
dag = dag.partial_subset(task_ids_or_regex=root,
include_downstream=False, include_upstream=True)
@@ -2840,6 +2843,8 @@ class Airflow(AirflowBaseView):
"numRuns": num_runs_options,
}
),
+ included_events_raw=included_events_raw,
+ excluded_events_raw=excluded_events_raw,
)
@expose("/calendar")
diff --git a/tests/www/views/test_views.py b/tests/www/views/test_views.py
index a588cb1864..27f096403f 100644
--- a/tests/www/views/test_views.py
+++ b/tests/www/views/test_views.py
@@ -94,6 +94,8 @@ def test_redoc_should_render_template(capture_templates,
admin_client):
"openapi_spec_url": "/api/v1/openapi.yaml",
"rest_api_enabled": True,
"get_docs_url": get_docs_url,
+ "excluded_events_raw": "",
+ "included_events_raw": "",
}