JavaScriptを有効にしてください

Hugo on Azure Static Web Apps を Application Insights で監視し隊

 ·   4 分で読めます

今回は Hugo on Azure Static Web Apps に移行した本ブログを Application Insights で監視させたので備忘録です

Application Insight とは

Application Insights は Azure の監視サービス群である Azure Monitor に含まれる監視サービスです
特にアプリの監視をする際に用いられます
以前、WordPress で本ブログを運用していた時は専用のアドオンが提供されており、簡単に導入できました
拡張機能がなくても、Application Insights はアプリケーションのコードに数行追加するだけで監視設定が完了します
今回は Hugo on Static Web Apps の本ブログを Application Insights で監視してみます

Application Insights 導入方法

Application Insights の導入方法はいくつかあります

  1. Azure Monitor Application Insights の自動インストルメンテーション - Azure Monitor | Microsoft Learn
  2. Application Insights SDK サポート ガイダンス - Azure Monitor | Microsoft Learn
  3. .NET、Java、Node.js、Python アプリケーション用の Azure Monitor OpenTelemetry を有効にする - Azure Monitor | Microsoft Learn

1つ目の方法はコードに変更を加えることなく、監視設定をすることができます
簡単に導入できますが、適用できる環境は限定的で、今回は適していませんでした

portal02
Static Web Apps のメニューを見ると追加の設定が必要そう。よくわからなかった:

2つ目の方法はアプリケーションに数行のコードを入れるだけで導入が完了します
従来はインストルメンテーションキーが使われていましたが、2025年3月31日にサポートが終了するとアナウンスがされています。現在は接続文字列を使うことが推奨されており、今回はこれを採用します

3つ目は2つ目と同じ考え方で、OpenTelemetry ベースの仕組みが採用されいます (あまり よくわかってないです)

Hugo のカスタマイズ

Hugo ではテーマを選択しブログを構築します
本ブログでは zzo を採用しています
zzo の導入手順に従って git submodule を使っています
Installation – Z Themes Documentation

この方法だと、テーマのファイルを直接編集して Application Insights を導入することができません
Hugo ではテーマのコードを直接変更しなくても root/layouts/ 配下に追加・上書きしたいファイルを置くことでテーマを修正することができます
今回はこの仕組みを使って、Hugo をカスタマイズしました

実施したこと

まずは Hugo テーマのどのファイルなんの修正を加えるべきなのかを把握しました
海外の方のブログを見てみると、インストルメンテーションキーを使った構成方法がかいてありました。今回はこれを接続文字列に置き換えることにしました

1. 接続文字列の使い方を確認

Application Insights の接続文字列 - Azure Monitor | Microsoft Learn を確認しました
また、インストルメンテーションキーを使った構成例を確認しました
Adding Application Insights to your Hugo website - Gary Jackson

2. 追加・修正対象ファイルを root/layouts に配置

root/layouts/partials/appinsights.html のファイルを作成しました

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<script type="text/javascript">
    !function(T,l,y){var S=T.location,k="script",D="instrumentationKey",C="ingestionendpoint",I="disableExceptionTracking",E="ai.device.",b="toLowerCase",w="crossOrigin",N="POST",e="appInsightsSDK",t=y.name||"appInsights";(y.name||T[e])&&(T[e]=t);var n=T[t]||function(d){var g=!1,f=!1,m={initialize:!0,queue:[],sv:"5",version:2,config:d};function v(e,t){var n={},a="Browser";return n[E+"id"]=a[b](),n[E+"type"]=a,n["ai.operation.name"]=S&&S.pathname||"_unknown_",n["ai.internal.sdkVersion"]="javascript:snippet_"+(m.sv||m.version),{time:function(){var e=new Date;function t(e){var t=""+e;return 1===t.length&&(t="0"+t),t}return e.getUTCFullYear()+"-"+t(1+e.getUTCMonth())+"-"+t(e.getUTCDate())+"T"+t(e.getUTCHours())+":"+t(e.getUTCMinutes())+":"+t(e.getUTCSeconds())+"."+((e.getUTCMilliseconds()/1e3).toFixed(3)+"").slice(2,5)+"Z"}(),iKey:e,name:"Microsoft.ApplicationInsights."+e.replace(/-/g,"")+"."+t,sampleRate:100,tags:n,data:{baseData:{ver:2}}}}var h=d.url||y.src;if(h){function a(e){var t,n,a,i,r,o,s,c,u,p,l;g=!0,m.queue=[],f||(f=!0,t=h,s=function(){var e={},t=d.connectionString;if(t)for(var n=t.split(";"),a=0;a<n.length;a++){var i=n[a].split("=");2===i.length&&(e[i[0][b]()]=i[1])}if(!e[C]){var r=e.endpointsuffix,o=r?e.location:null;e[C]="https://"+(o?o+".":"")+"dc."+(r||"services.visualstudio.com")}return e}(),c=s[D]||d[D]||"",u=s[C],p=u?u+"/v2/track":d.endpointUrl,(l=[]).push((n="SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details)",a=t,i=p,(o=(r=v(c,"Exception")).data).baseType="ExceptionData",o.baseData.exceptions=[{typeName:"SDKLoadFailed",message:n.replace(/\./g,"-"),hasFullStack:!1,stack:n+"\nSnippet failed to load ["+a+"] -- Telemetry is disabled\nHelp Link: https://go.microsoft.com/fwlink/?linkid=2128109\nHost: "+(S&&S.pathname||"_unknown_")+"\nEndpoint: "+i,parsedStack:[]}],r)),l.push(function(e,t,n,a){var i=v(c,"Message"),r=i.data;r.baseType="MessageData";var o=r.baseData;return o.message='AI (Internal): 99 message:"'+("SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details) ("+n+")").replace(/\"/g,"")+'"',o.properties={endpoint:a},i}(0,0,t,p)),function(e,t){if(JSON){var n=T.fetch;if(n&&!y.useXhr)n(t,{method:N,body:JSON.stringify(e),mode:"cors"});else if(XMLHttpRequest){var a=new XMLHttpRequest;a.open(N,t),a.setRequestHeader("Content-type","application/json"),a.send(JSON.stringify(e))}}}(l,p))}function i(e,t){f||setTimeout(function(){!t&&m.core||a()},500)}var e=function(){var n=l.createElement(k);n.src=h;var e=y[w];return!e&&""!==e||"undefined"==n[w]||(n[w]=e),n.onload=i,n.onerror=a,n.onreadystatechange=function(e,t){"loaded"!==n.readyState&&"complete"!==n.readyState||i(0,t)},n}();y.ld<0?l.getElementsByTagName("head")[0].appendChild(e):setTimeout(function(){l.getElementsByTagName(k)[0].parentNode.appendChild(e)},y.ld||0)}try{m.cookie=l.cookie}catch(p){}function t(e){for(;e.length;)!function(t){m[t]=function(){var e=arguments;g||m.queue.push(function(){m[t].apply(m,e)})}}(e.pop())}var n="track",r="TrackPage",o="TrackEvent";t([n+"Event",n+"PageView",n+"Exception",n+"Trace",n+"DependencyData",n+"Metric",n+"PageViewPerformance","start"+r,"stop"+r,"start"+o,"stop"+o,"addTelemetryInitializer","setAuthenticatedUserContext","clearAuthenticatedUserContext","flush"]),m.SeverityLevel={Verbose:0,Information:1,Warning:2,Error:3,Critical:4};var s=(d.extensionConfig||{}).ApplicationInsightsAnalytics||{};if(!0!==d[I]&&!0!==s[I]){var c="onerror";t(["_"+c]);var u=T[c];T[c]=function(e,t,n,a,i){var r=u&&u(e,t,n,a,i);return!0!==r&&m["_"+c]({message:e,url:t,lineNumber:n,columnNumber:a,error:i}),r},d.autoExceptionInstrumented=!0}return m}(y.cfg);function a(){y.onInit&&y.onInit(n)}(T[t]=n).queue&&0===n.queue.length?(n.queue.push(a),n.trackPageView({})):a()}(window,document,{
    src: "https://js.monitor.azure.com/scripts/b/ai.2.min.js", // The SDK URL Source
    // name: "appInsights", // Global SDK Instance name defaults to "appInsights" when not supplied
    // ld: 0, // Defines the load delay (in ms) before attempting to load the sdk. -1 = block page load and add to head. (default) = 0ms load after timeout,
    // useXhr: 1, // Use XHR instead of fetch to report failures (if available),
    crossOrigin: "anonymous", // When supplied this will add the provided value as the cross origin attribute on the script tag
    // onInit: null, // Once the application insights instance has loaded and initialized this callback function will be called with 1 argument -- the sdk instance (DO NOT ADD anything to the sdk.queue -- as they won't get called)
    cfg: { // Application Insights Configuration
      connectionString:"InstrumentationKey=xxxxx"
    }});
</script>

※最後の接続文字列は自分の環境に合わせます

この appinsights.html を参照するように root/layouts/_default/baseof.html に修正を加えます
まず、zzo で使用されている baseof.html を hugo-theme-zzo/layouts/_default/baseof.html at master · zzossig/hugo-theme-zzo · GitHub からコピペしてきます

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html lang="{{ .Site.Language.Lang }}" dir="{{ $.Param "languagedir" | default "ltr" }}">

<head prefix="og: http://ogp.me/ns#">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>{{ block "title" . }}{{ .Title }} – {{ .Site.Title }}{{ end }}</title>
    {{ partial "head/scripts" . }}    
    {{ partial "head/styles" . }}
    {{ partial "head/meta" . }}
    {{ partial "head/meta_json_ld" . }}
    {{ partial "head/services" . }}
    {{ partial "head/custom-head" . }}
</head>

<body id="root" class="theme__{{ index .Site.Params.themeOptions 0 }}">
    <script>
        var localTheme = localStorage.getItem('theme');
        if (localTheme) {
            document.getElementById('root').className = 'theme__' + localTheme;
        }
    </script>
    <div id="container">
        {{ partial "body/main-left" . }}
        <div class="wrapper" data-type="{{ .Type }}" data-kind="{{ .Kind }}">
            {{ partial "navbar/site-nav" . }}
            {{ partial "header/site-header" . }}
            {{ block "main" . }}{{ end }}
            {{ partial "body/custom-body" . }}
            {{ partial "footer/site-footer" . }}
        </div>
        {{ partial "body/main-right" . }}
    </div>
</body>

</html>

ここに先ほど作成した appinsights.html を参照するように追記します

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="{{ .Site.Language.Lang }}" dir="{{ $.Param "languagedir" | default "ltr" }}">

<head prefix="og: http://ogp.me/ns#">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>{{ block "title" . }}{{ .Title }} – {{ .Site.Title }}{{ end }}</title>
    {{ partial "head/scripts" . }}    
    {{ partial "head/styles" . }}
    {{ partial "head/meta" . }}
    {{ partial "head/meta_json_ld" . }}
    {{ partial "head/services" . }}
    {{ partial "head/custom-head" . }}
    {{ partial "appinsights.html" . }}
</head>

<body id="root" class="theme__{{ index .Site.Params.themeOptions 0 }}">
    <script>
        var localTheme = localStorage.getItem('theme');
        if (localTheme) {
            document.getElementById('root').className = 'theme__' + localTheme;
        }
    </script>
    <div id="container">
        {{ partial "body/main-left" . }}
        <div class="wrapper" data-type="{{ .Type }}" data-kind="{{ .Kind }}">
            {{ partial "navbar/site-nav" . }}
            {{ partial "header/site-header" . }}
            {{ block "main" . }}{{ end }}
            {{ partial "body/custom-body" . }}
            {{ partial "footer/site-footer" . }}
        </div>
        {{ partial "body/main-right" . }}
    </div>
</body>

</html>

14行目に {{ partial “appinsights.html” . }} を追加しただけです

code01
追加したファイル:

検証

しばらく放置して、ブログの閲覧ログを収集しました
無事にブログの分析ができるようにログが収集されていました

portal01
Application Insigts でのページの分析:

まとめ

Hugo on Static Web Apps に Application Insights を導入しました
どんな記事が人気なのか気になっていたので、これで分析してみたいと思います

参考

共有

Kento
著者
Kento
2020年に新卒で IT 企業に入社. インフラエンジニア(主にクラウド)として活動中