11---
22title : " App Canary - Application Health Monitor"
3- format : email
3+ format :
4+ email :
5+ page-layout : full
46---
57
68``` {python}
79#| echo: false
10+ #| label: setup
811
912import json
1013import os
@@ -47,11 +50,21 @@ else:
4750
4851# Instantiate a Connect client using posit-sdk where api_key and url are automatically read from our environment vars
4952client = connect.Client()
53+ ```
54+
55+ ``` {python}
56+ #| echo: false
57+ #| label: data-gathering
5058
5159if not show_instructions:
5260 # Proceed to validate our monitored GUIDS
61+
5362 # Headers for Connect API
54- headers = {"Authorization": f"Key {api_key}"}
63+ headers = {
64+ "Authorization": f"Key {api_key}",
65+ # Set a custom user agent to enable filtering App Canary activity in Connect instrumentation data
66+ "User-Agent": "AppCanary/1.0",
67+ }
5568
5669 # Check if server is reachable, would only be a potential problem during local dev
5770 try:
@@ -96,15 +109,54 @@ if not show_instructions:
96109 "guid": guid
97110 }
98111
112+ def get_user(user_guid):
113+ try:
114+ user = client.users.get(user_guid)
115+ return user
116+ except Exception as e:
117+ raise RuntimeError(f"Error getting user: {str(e)}")
118+
99119 # Function to validate app health (simple HTTP 200 check)
100120 def validate_app(guid):
101121 # Get app details
102122 content = get_content(guid)
103- app_name = content.get("title", "Unknown")
123+ app_name = content.get("title", "")
124+ # Title is optional, if not set use name
125+ if not app_name:
126+ app_name = content.get("name", "")
104127
105- # Extract content_url if available
128+ # Ensure we have a valid URL
106129 dashboard_url = content.get("dashboard_url", "")
130+ content_guid = content.get("guid", guid) # Use the passed guid if not in content
131+
132+ # Initialize default owner information
133+ owner_email = ""
134+ owner_full_name = ""
107135
136+ # Get additional data if we don't have an error when attempting to get the content details
137+ if not str(app_name).startswith("ERROR:"):
138+ # Get content owner details
139+ try:
140+ owner_guid = content.get("owner_guid")
141+ if owner_guid:
142+ # Get owner details
143+ owner = get_user(owner_guid)
144+ owner_email = owner.get("email", "")
145+ owner_first_name = owner.get("first_name", "")
146+ owner_last_name = owner.get("last_name", "")
147+ owner_full_name = f"{owner_first_name} {owner_last_name}".strip()
148+ if not owner_full_name: # Handle case where both names are empty
149+ owner_full_name = "Unknown"
150+ except Exception as e:
151+ # If there's an error getting the owner, keep the defaults
152+ print(f"Warning: Could not retrieve owner for {guid}: {str(e)}")
153+
154+ # Compose URL to logs if we have a dashboard URL, only owner/editor have access
155+ if dashboard_url and content.get("app_role") != "viewer":
156+ logs_url = f"{dashboard_url}/logs"
157+ else:
158+ logs_url = ""
159+
108160 try:
109161 app_url = f"{connect_server}/content/{guid}"
110162 app_response = requests.get(
@@ -115,18 +167,30 @@ if not show_instructions:
115167 )
116168
117169 return {
170+ # App details
118171 "guid": guid,
119172 "name": app_name,
120173 "dashboard_url": dashboard_url,
174+ "logs_url": logs_url,
175+ # Owner details
176+ "owner_name": owner_full_name,
177+ "owner_email": owner_email,
178+ # Monitoring status
121179 "status": "PASS" if app_response.status_code >= 200 and app_response.status_code < 300 else "FAIL",
122180 "http_code": app_response.status_code
123181 }
124182
125183 except Exception as e:
126184 return {
185+ # App details
127186 "guid": guid,
128187 "name": app_name,
129188 "dashboard_url": dashboard_url,
189+ "logs_url": logs_url,
190+ # Owner details
191+ "owner_name": owner_full_name,
192+ "owner_email": owner_email,
193+ # Monitoring status
130194 "status": "FAIL",
131195 "http_code": str(e)
132196 }
@@ -139,28 +203,14 @@ if not show_instructions:
139203 # Convert results to DataFrame for easy display
140204 df = pd.DataFrame(results)
141205
142- # Reorder columns to put name first
143- # Create a dynamic column order with name first, status and http_code last
144- if 'name' in df.columns:
145- cols = ['name'] # Start with name
146- # Add any other columns except name, status, and http_code
147- middle_cols = [col for col in df.columns if col not in ['name', 'status', 'http_code']]
148- cols.extend(middle_cols)
149- # Add status and http_code at the end
150- if 'status' in df.columns:
151- cols.append('status')
152- if 'http_code' in df.columns:
153- cols.append('http_code')
154- # Reorder the DataFrame
155- df = df[cols]
156-
157206 # Store the current time
158207 check_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
159208```
160209
161210
162211``` {python}
163212#| echo: false
213+ #| label: display
164214
165215# Create a table with basic styling
166216if not show_instructions and not df.empty:
@@ -175,34 +225,94 @@ if not show_instructions and not df.empty:
175225 <div style="padding: 5px 0;">{canary_guids_str}</div>
176226 </div>
177227 """))
178-
228+
179229 # First create links for name and guid columns
180230 df_display = df.copy()
181231
182- # Process the DataFrame rows to add HTML links
232+ # Process the DataFrame rows to add HTML links and format owner info
183233 for i in range(len(df_display)):
184- if not pd.isna(df_display.loc[i, 'dashboard_url']) and df_display.loc[i, 'dashboard_url']:
234+ # Format app name links only (not guid)
235+ if 'dashboard_url' in df_display.columns and not pd.isna(df_display.loc[i, 'dashboard_url']) and df_display.loc[i, 'dashboard_url']:
185236 url = df_display.loc[i, 'dashboard_url']
186- df_display.loc[i, 'name'] = f"<a href='{url}' target='_blank'>{df_display.loc[i, 'name']}</a>"
187- df_display.loc[i, 'guid'] = f"<a href='{url}' target='_blank'>{df_display.loc[i, 'guid']}</a>"
237+ app_name = df_display.loc[i, 'name']
238+ app_guid = df_display.loc[i, 'guid']
239+
240+ # Create simple markdown link for name only
241+ df_display.loc[i, 'name'] = f"<a href='{url}' target='_blank'>{app_name}</a>"
242+
243+ # Format logs URL using markdown instead of HTML (similar to owner email)
244+ if 'logs_url' in df_display.columns and df_display.loc[i, 'logs_url'] is not None and str(df_display.loc[i, 'logs_url']).strip():
245+ # Use a simple icon since we can't use custom SVG in emails easily
246+ logs_icon = "📋"
247+ logs_url = df_display.loc[i, 'logs_url']
248+ df_display.loc[i, 'logs'] = f"<a href='{logs_url}' target='_blank' style='text-decoration:none;'>{logs_icon}</a>"
249+
250+ else:
251+ df_display.loc[i, 'logs'] = ""
252+
253+ # Format owner name with email icon link
254+ owner_name = df_display.loc[i, 'owner_name'] if not pd.isna(df_display.loc[i, 'owner_name']) else "Unknown"
255+ owner_email = df_display.loc[i, 'owner_email'] if not pd.isna(df_display.loc[i, 'owner_email']) else ""
256+
257+ if owner_email:
258+ # Use a simple icon since we can't use custom SVG in emails easily
259+ email_icon = "✉️"
260+ df_display.loc[i, 'owner_display'] = f"{owner_name} <a href='mailto:{owner_email}' style='text-decoration:none;'>{email_icon}</a>"
261+ else:
262+ df_display.loc[i, 'owner_display'] = owner_name
188263
189264 # Remove dashboard_url column since the links are embedded in the other columns
190265 if 'dashboard_url' in df_display.columns:
191266 df_display = df_display.drop(columns=['dashboard_url'])
192267
268+ # Remove raw owner columns in favor of our formatted display column
269+ if 'owner_name' in df_display.columns:
270+ df_display = df_display.drop(columns=['owner_name'])
271+ if 'owner_email' in df_display.columns:
272+ df_display = df_display.drop(columns=['owner_email'])
273+
274+ # Reorder columns to match requested layout
275+ column_order = ['name', 'guid', 'status', 'http_code', 'logs', 'owner_display']
276+ # Only include columns that exist in df_display
277+ ordered_columns = [col for col in column_order if col in df_display.columns]
278+ df_display = df_display[ordered_columns]
279+
193280 # Create GT table
194281 gt_tbl = GT(df_display)
195282
196- # Apply styling to status column
283+ # Tell great_tables to render markdown content in these columns
284+ gt_tbl = gt_tbl.fmt_markdown(columns=['name', 'owner_display', 'logs'])
285+
286+ # Apply styling to columns
197287 gt_tbl = (gt_tbl
288+ # Status column styling - green for PASS, red for FAIL
198289 .tab_style(
199290 style.fill("green"),
200291 locations=loc.body(columns="status", rows=lambda df: df["status"] == "PASS")
201292 )
202293 .tab_style(
203294 style.fill("red"),
204295 locations=loc.body(columns="status", rows=lambda df: df["status"] == "FAIL")
205- ))
296+ )
297+ # Set column labels for better presentation
298+ .cols_label(
299+ owner_display="Owner",
300+ name="Name",
301+ guid="Content GUID",
302+ status="Status",
303+ http_code="HTTP Code",
304+ logs="Logs"
305+ )
306+ # Override default column alignment for better presentation
307+ .cols_align(
308+ align='center',
309+ columns=["status", "http_code", "logs"]
310+ )
311+ .tab_options(
312+ container_width="100%"
313+ )
314+ )
315+
206316elif show_instructions:
207317 # Create a callout box for instructions
208318 instructions_html = ""
0 commit comments