Skip to content

Commit 64dd971

Browse files
authored
App Canary: display GUIDs and better instructions (#178)
* Implement posit-sdk * Provide better formatted instructions, and display the CANARY_GUIDS as text
1 parent a63ece7 commit 64dd971

File tree

3 files changed

+101
-47
lines changed

3 files changed

+101
-47
lines changed

extensions/app-canary/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ app-canary.html
1111
preview.html
1212

1313
/.posit/
14+
15+
.envrc

extensions/app-canary/app-canary.qmd

Lines changed: 98 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,53 +6,54 @@ format: email
66
```{python}
77
#| echo: false
88
9+
import json
910
import os
1011
import requests
1112
import datetime
1213
import pandas as pd
13-
from great_tables import GT, style, loc, exibble, html
14+
from posit import connect
15+
from great_tables import GT, style, loc, html
16+
from IPython.display import HTML, display
1417
1518
# Used to display on-screen setup instructions if environment variables are missing
1619
show_instructions = False
1720
instructions = []
1821
gt_tbl = None
1922
20-
# Read CONNECT_SERVER from environment, should be configured automatically when run on Connect
23+
# Read CONNECT_SERVER from environment, this is automatically configured on Connect, set manually for local dev
2124
connect_server = os.environ.get("CONNECT_SERVER", "")
2225
if not connect_server:
2326
show_instructions = True
2427
instructions.append("Please set the CONNECT_SERVER environment variable.")
2528
26-
# Read CONNECT_API_KEY from environment, should be configured automatically when run on Connect
29+
# Read CONNECT_API_KEY from environment, this is automatically configured on Connect, set manually for local dev
2730
api_key = os.environ.get("CONNECT_API_KEY", "")
2831
if not api_key:
2932
show_instructions = True
3033
instructions.append("Please set the CONNECT_API_KEY environment variable.")
3134
32-
# Read CANARY_GUIDS from environment, needs to be manually configured on Connect
33-
app_guid_str = os.environ.get("CANARY_GUIDS", "")
34-
if not app_guid_str:
35+
# Read CANARY_GUIDS from environment, needs to be manually configured on Connect and for local dev
36+
canary_guids_str = os.environ.get("CANARY_GUIDS", "")
37+
if not canary_guids_str:
3538
show_instructions = True
3639
instructions.append("Please set the CANARY_GUIDS environment variable. It should be a comma separated list of GUID you wish to monitor.")
37-
app_guids = []
40+
canary_guids = []
3841
else:
3942
# Clean up the GUIDs
40-
app_guids = [guid.strip() for guid in app_guid_str.split(',') if guid.strip()]
41-
if not app_guids:
43+
canary_guids = [guid.strip() for guid in canary_guids_str.split(',') if guid.strip()]
44+
if not canary_guids:
4245
show_instructions = True
43-
instructions.append("CANARY_GUIDS environment variable is empty or contains only whitespace. It should be a comma separated list of GUID you wish to monitor. Raw CANARY_GUIDS value: '{app_guid_str}'")
46+
instructions.append(f"CANARY_GUIDS environment variable is set but is empty or contains only whitespace. It should be a comma separated list of GUID you wish to monitor. Raw CANARY_GUIDS value: '{canary_guids_str}'")
4447
45-
if show_instructions:
46-
# We'll use this flag later to display instructions instead of results
47-
results = []
48-
df = pd.DataFrame() # Empty DataFrame
49-
check_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
50-
else:
51-
# Continue with normal execution
48+
# Instantiate a Connect client using posit-sdk where api_key and url are automatically read from our environment vars
49+
client = connect.Client()
50+
51+
if not show_instructions:
52+
# Proceed to validate our monitored GUIDS
5253
# Headers for Connect API
5354
headers = {"Authorization": f"Key {api_key}"}
5455
55-
# Check if server is reachable
56+
# Check if server is reachable, would only be a potential problem during local dev
5657
try:
5758
server_check = requests.get(
5859
f"{connect_server}/__ping__",
@@ -64,28 +65,45 @@ else:
6465
raise RuntimeError(f"Connect server at {connect_server} is unavailable: {str(e)}")
6566
6667
# Function to get app details from Connect API
67-
def get_app_details(guid):
68+
def get_content(guid):
6869
try:
6970
# Get app details from Connect API
70-
app_details_url = f"{connect_server}/__api__/v1/content/{guid}"
71-
app_details_response = requests.get(
72-
app_details_url,
73-
headers=headers,
74-
timeout=5
75-
)
76-
app_details_response.raise_for_status()
77-
return app_details_response.json()
78-
except Exception:
79-
return {"title": "Unknown", "guid": guid}
71+
content = client.content.get(guid)
72+
return content
73+
except Exception as e:
74+
# Initialize default error message
75+
error_message = str(e)
76+
77+
# posit-sdk will return a ClientError if there is a problem getting the guid, parse the error message
78+
if isinstance(e, connect.errors.ClientError):
79+
try:
80+
# ClientError from posit-connect SDK stores error as string that contains JSON
81+
# Convert the string representation to a dict
82+
error_data = json.loads(str(e))
83+
if isinstance(error_data, dict):
84+
# Extract the specific error message
85+
if "error_message" in error_data:
86+
error_message = error_data["error_message"]
87+
elif "error" in error_data:
88+
error_message = error_data["error"]
89+
except json.JSONDecodeError:
90+
# If parsing fails, keep the original error message
91+
pass
92+
93+
# Return content with error in title
94+
return {
95+
"title": f"ERROR: {error_message}",
96+
"guid": guid
97+
}
8098
8199
# Function to validate app health (simple HTTP 200 check)
82100
def validate_app(guid):
83101
# Get app details
84-
app_details = get_app_details(guid)
85-
app_name = app_details.get("title", "Unknown")
102+
content = get_content(guid)
103+
app_name = content.get("title", "Unknown")
86104
87105
# Extract content_url if available
88-
dashboard_url = app_details.get("dashboard_url", "")
106+
dashboard_url = content.get("dashboard_url", "")
89107
90108
try:
91109
app_url = f"{connect_server}/content/{guid}"
@@ -115,7 +133,7 @@ else:
115133
116134
# Check all apps and collect results
117135
results = []
118-
for guid in app_guids:
136+
for guid in canary_guids:
119137
results.append(validate_app(guid))
120138
121139
# Convert results to DataFrame for easy display
@@ -147,6 +165,17 @@ else:
147165
# Create a table with basic styling
148166
if not show_instructions and not df.empty:
149167
168+
# Format the canary_guids as a string for display
169+
canary_guids_str = ", ".join(canary_guids)
170+
171+
# Use HTML to create a callout box
172+
display(HTML(f"""
173+
<div style="border: 1px solid #ccc; border-radius: 8px; padding: 10px; margin-bottom: 15px; background-color: #f8f9fa;">
174+
<div style="margin-top: 0; padding-bottom: 8px; border-bottom: 1px solid #eaecef; font-weight: bold; font-size: 1.2em;">Monitored GUIDs</div>
175+
<div style="padding: 5px 0;">{canary_guids_str}</div>
176+
</div>
177+
"""))
178+
150179
# First create links for name and guid columns
151180
df_display = df.copy()
152181
@@ -174,20 +203,43 @@ if not show_instructions and not df.empty:
174203
style.fill("red"),
175204
locations=loc.body(columns="status", rows=lambda df: df["status"] == "FAIL")
176205
))
177-
178-
# Display instructions if setup failed
179-
if show_instructions:
180-
# Create a DataFrame with instructions
181-
instructions_df = pd.DataFrame({
182-
"Setup has failed": instructions
183-
})
206+
elif show_instructions:
207+
# Create a callout box for instructions
208+
instructions_html = ""
209+
for instruction in instructions:
210+
instructions_html += f"<div style='margin-bottom: 10px;'>{instruction}</div>"
184211
185-
# Create a GT table for instructions
186-
gt_tbl = GT(instructions_df)
187-
gt_tbl = (gt_tbl
188-
.tab_source_note(
189-
source_note=html("See Posit Connect documentation for <a href='https://docs.posit.co/connect/user/content-settings/#content-vars' target='_blank'>Vars (environment variables)</a>")
190-
))
212+
display(HTML(f"""
213+
<div style="border: 1px solid #cc0000; border-radius: 8px; padding: 10px; margin-bottom: 15px; background-color: #fff8f8;">
214+
<div style="margin-top: 0; padding-bottom: 8px; border-bottom: 1px solid #eaecef; color: #cc0000; font-weight: bold; font-size: 1.2em;">⚠️ Setup Instructions</div>
215+
{instructions_html}
216+
<div style="padding-top: 8px; font-size: 0.9em; border-top: 1px solid #eaecef;">
217+
See Posit Connect documentation for <a href='https://docs.posit.co/connect/user/content-settings/#content-vars' target='_blank'>Vars (environment variables)</a>
218+
</div>
219+
</div>
220+
"""))
221+
222+
# Set gt_tbl to None since we're using HTML display instead
223+
gt_tbl = None
224+
else:
225+
# We should only hit this catchall if the dataframe is empty (a likely error) and there are no instructions
226+
display(HTML(f"""
227+
<div style="border: 1px solid #f0ad4e; border-radius: 8px; padding: 10px; margin-bottom: 15px; background-color: #fcf8e3;">
228+
<div style="margin-top: 0; padding-bottom: 8px; border-bottom: 1px solid #eaecef; color: #cc0000; font-weight: bold; font-size: 1.2em;">⚠️ No results available</div>
229+
<div style="padding: 5px 0;">
230+
No monitoring results were found. This could be because:
231+
<ul>
232+
<li>No valid GUIDs were provided</li>
233+
<li>There was an issue connecting to the specified content</li>
234+
<li>The environment is properly configured but there was an error that caused no data to be returned</li>
235+
</ul>
236+
<p>Please check your CANARY_GUIDS environment variable and ensure it contains valid content identifiers.</p>
237+
</div>
238+
</div>
239+
"""))
240+
241+
# Set gt_tbl to None since we're using HTML display instead
242+
gt_tbl = None
191243
192244
# Compute if we should send an email, only send if at least one app has a failure
193245
if 'df' in locals() and 'status' in df.columns:
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
ipython
22
great_tables
33
pandas
4+
posit-sdk
45
requests
5-
css-inline

0 commit comments

Comments
 (0)