O
One of the most underrated things is checking the state of your database and APEX apps. You deploy things to Prod, nothing crashes (so far), so you move on. This is a strategy I've seen on many projects: no unit tests, barely some random user tests, and that's it. On top of that, in some projects I don't even have access to UAT. I only have access to Dev, but I somehow still have to make things work, even without read-only access anywhere else.
So, how would you check things in a place where you don't have any access? Fortunately, I can at least send emails. So today, it's about the first email.
What to check?
The backend side contains mostly things for developers, like errors, logs, and security issues, or just to see what changed or what is different. Each link will take you to the source code of the appropriate view (because views are awesome: they keep the package readable and your reports flexible, since you can add your own views or remove the ones you don't need). I won't copy-paste the code here, so I can show you the more interesting parts, how to make this whole thing flexible.
- Versions (of database, APEX, ORDS)
- Invalid Objects
- Disabled Objects (because I face this a lot)
- Compile Errors
- Schedulers (very important, can affect the whole system)
- Materialized Views (also important, they might get stuck)
- Synonyms (with grants and referenced object status)
- Failed Authentications (in APEX)
- Missing VPD Policies
- Broken APEX Components (you need to scan the apps first)
- Workspace Errors (also APEX related)
- APEX Debug Messages
- Mail Queue Errors (to see if you have mails stuck in the queue)
- Web Service Calls (to see how many calls you are doing)
Technical Part
How would you query 20 views and convert the results into HTML tables so you can stitch them together into one email? And how would you do it in a smart way, so you don't need to know the view names or the structure (columns)?
First, we need a cursor which will find all relevant views (CORE_DAILY_%_V) and sort them (by the comments).
CURSOR related_views (
in_prefix VARCHAR2 -- like CORE_DAILY
)
IS
SELECT
t.view_name,
COALESCE(
REGEXP_SUBSTR(c.comments, '\|\s*([^\|]+)', 1, 1, NULL, 1),
INITCAP(REPLACE(REGEXP_SUBSTR(t.view_name, '^' || in_prefix || '_(.*)_V$', 1, 1, NULL, 1), '_', ' '))
) AS header3,
REGEXP_SUBSTR(c.comments, '\|\s*([^\|]+)', 1, 2, NULL, 1) AS header2
FROM user_views t
LEFT JOIN user_tab_comments c
ON c.table_name = t.view_name
WHERE t.view_name LIKE in_prefix || '\_%\_V' ESCAPE '\'
AND t.view_name NOT LIKE in_prefix || '\_\_%\_V' ESCAPE '\'
ORDER BY
c.comments || t.view_name;
I utilized the view comments to handle the order and headers. You could use a regular table or annotations. In the comments you can also define the table header and main header (to group tables). The pattern is "Order# | Header | Group header".
SELECT
t.table_name,
t.comments
FROM user_tab_comments t
WHERE t.table_name LIKE 'CORE_DAILY_%\_V' ESCAPE '\'
ORDER BY 2;
| Table Name | Comments |
|---|---|
| CORE_DAILY_VERSIONS_V | 10 | Versions | Database Overview |
| CORE_DAILY_INVALID_OBJECTS_V | 11 | Invalid Objects |
| CORE_DAILY_DISABLED_OBJECTS_V | 12 | Disabled Objects |
| CORE_DAILY_COMPILE_ERRORS_V | 13 | Compile Errors |
| CORE_DAILY_SCHEDULERS_V | 20 | Schedulers | Schedulers & Materialized Views |
| CORE_DAILY_MATERIALIZED_VIEWS_V | 21 | Materialized Views |
| CORE_DAILY_SYNONYMS_V | 22 | Synonyms |
| CORE_DAILY_FAILED_AUTHENTICATION_V | 30 | Failed Authentication | Security Issues |
| CORE_DAILY_MISSING_VPD_POLICIES_V | 31 | Missing VPD Policies |
| CORE_DAILY_BROKEN_APEX_COMPONENTS_V | 40 | Broken APEX Components | APEX Issues |
| CORE_DAILY_WORKSPACE_ERRORS_V | 41 | Workspace Errors |
| CORE_DAILY_APEX_DEBUG_MESSAGES_V | 42 | APEX Debug Messages |
| CORE_DAILY_MAIL_QUEUE_ERRORS_V | 43 | Mail Queue Errors |
| CORE_DAILY_WEB_SERVICE_CALLS_V | 44 | Web Service Calls |
The whole header structure will be more clear after you read the generated email.
Second, we need to convert generic cursor to HTML table:
FUNCTION get_content (
io_cursor IN OUT SYS_REFCURSOR,
--
in_header VARCHAR2 := NULL
)
RETURN CLOB
AS
v_cursor PLS_INTEGER;
v_desc DBMS_SQL.DESC_TAB;
v_cols PLS_INTEGER;
v_number NUMBER;
v_date DATE;
v_value VARCHAR2(4000);
v_header VARCHAR2(32767);
v_line VARCHAR2(32767);
v_align VARCHAR2(2000);
v_style VARCHAR2(2000);
v_out CLOB := EMPTY_CLOB();
--
TYPE t_array IS TABLE OF PLS_INTEGER INDEX BY VARCHAR2(128);
v_styles t_array;
BEGIN
v_cursor := DBMS_SQL.TO_CURSOR_NUMBER(io_cursor);
-- get column names
DBMS_SQL.DESCRIBE_COLUMNS(v_cursor, v_cols, v_desc);
-- process headers
FOR i IN 1 .. v_cols LOOP
-- retrive value and do formatting
v_align := ' align="left"';
--
IF v_desc(i).col_type = DBMS_SQL.NUMBER_TYPE THEN
DBMS_SQL.DEFINE_COLUMN(v_cursor, i, v_number);
v_align := ' align="right"';
ELSIF v_desc(i).col_type = DBMS_SQL.DATE_TYPE THEN
DBMS_SQL.DEFINE_COLUMN(v_cursor, i, v_date);
ELSE
DBMS_SQL.DEFINE_COLUMN(v_cursor, i, v_value, 4000);
END IF;
-- identify and store column position with style value
IF v_desc(i).col_name LIKE '%\_\_STYLE' ESCAPE '\' THEN
v_styles(REPLACE(RTRIM(v_desc(i).col_name, '_'), '__STYLE', '')) := i;
CONTINUE;
END IF;
--
v_line := v_line || '<th' || v_align || '>' || get_column_name(v_desc(i).col_name) || '</th>';
END LOOP;
--
v_out := v_out || TO_CLOB('<table cellpadding="5" cellspacing="0" border="1"><thead><tr>' || v_line || '</tr></thead><tbody>');
-- fetch data
v_line := '';
WHILE DBMS_SQL.FETCH_ROWS(v_cursor) > 0 LOOP
v_line := '';
--
FOR i IN 1 .. v_cols LOOP
-- process all columns except style ones
IF RTRIM(v_desc(i).col_name, '_') LIKE '%\_\_STYLE' ESCAPE '\' THEN
CONTINUE;
END IF;
--
v_align := '';
--
IF v_desc(i).col_type = DBMS_SQL.NUMBER_TYPE THEN
DBMS_SQL.COLUMN_VALUE(v_cursor, i, v_number);
v_value := TO_CHAR(v_number);
v_align := ' align="right"';
ELSIF v_desc(i).col_type = DBMS_SQL.DATE_TYPE THEN
DBMS_SQL.COLUMN_VALUE(v_cursor, i, v_date);
v_value := CASE WHEN v_date IS NOT NULL THEN TO_CHAR(v_date, 'YYYY-MM-DD HH24:MI') END;
ELSE
DBMS_SQL.COLUMN_VALUE(v_cursor, i, v_value);
END IF;
-- apply styles
IF v_styles.EXISTS(v_desc(i).col_name) THEN
DBMS_SQL.COLUMN_VALUE(v_cursor, v_styles(v_desc(i).col_name), v_style);
v_align := v_align
|| ' style="' || CASE
WHEN v_style = 'RED' THEN 'color: #f00;'
ELSE v_style END || '"';
END IF;
--
v_line := v_line || '<td' || v_align || '>' || TRIM(v_value) || '</td>';
END LOOP;
--
v_out := v_out || '<tr>' || v_line || '</tr>';
END LOOP;
-- cleanup
close_cursor(v_cursor);
-- prepend headers
IF v_line IS NULL THEN
v_out := '';
END IF;
--
RETURN
CASE WHEN in_header IS NOT NULL THEN TO_CLOB('<h3>' || in_header || '</h3>') END ||
v_out ||
CASE
WHEN v_line IS NOT NULL THEN TO_CLOB('</tbody></table><br />')
ELSE TO_CLOB('<p>No data found.</p><br />')
END;
--
EXCEPTION
WHEN core.app_exception THEN
close_cursor(v_cursor);
RAISE;
WHEN OTHERS THEN
close_cursor(v_cursor);
core.raise_error();
END;
One more cool thing about this is that you can define "__style" columns in your views and these values will get applied to the individual cells as style, and not be presented as data. And numbers are automatically right aligned.
SELECT
t.status,
CASE WHEN t.status != 'VALID' THEN 'color: red;' END AS status__style, -- not visible in table, but applied to "status" column
...
FROM t;
In the next part I will cover more APEX related checks and in the part after that I will cover REST services and how you can install this.
If you are keen to explore it right away, all the code is in the CORE_REPORTS package.
Also I made an update and the report names and columns names are not driven by the comments anymore, but they are stored in CORE_REPORT% tables.
Comments
Post a Comment