Skip to main content

Daily health checks, 1/3

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.


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_V10 | Versions | Database Overview
CORE_DAILY_INVALID_OBJECTS_V11 | Invalid Objects
CORE_DAILY_DISABLED_OBJECTS_V12 | Disabled Objects
CORE_DAILY_COMPILE_ERRORS_V13 | Compile Errors
CORE_DAILY_SCHEDULERS_V20 | Schedulers | Schedulers & Materialized Views
CORE_DAILY_MATERIALIZED_VIEWS_V21 | Materialized Views
CORE_DAILY_SYNONYMS_V22 | Synonyms
CORE_DAILY_FAILED_AUTHENTICATION_V30 | Failed Authentication | Security Issues
CORE_DAILY_MISSING_VPD_POLICIES_V31 | Missing VPD Policies
CORE_DAILY_BROKEN_APEX_COMPONENTS_V40 | Broken APEX Components | APEX Issues
CORE_DAILY_WORKSPACE_ERRORS_V41 | Workspace Errors
CORE_DAILY_APEX_DEBUG_MESSAGES_V42 | APEX Debug Messages
CORE_DAILY_MAIL_QUEUE_ERRORS_V43 | Mail Queue Errors
CORE_DAILY_WEB_SERVICE_CALLS_V44 | 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_JOBS package.


Comments