2026年3月20日金曜日

私が良く使うE8601DTの書式

 よく使う割に忘れてしまう日時の書式のメモです。

最近はタイムゾーンのオフセットを表示することが多いのですが、出力書式の選択を間違えると正しく日時、オフセットが表示されません。良く使うISOのE8601書式の例を示します。


私が良く使う書式は、E8601LX. です。それを使う理由は、海外の作業者に日時を指定するときにタイムゾーンのオフセットが無いと間違った日時でログ抽出されるからです。

2026年2月20日金曜日

DateMathで日時の指定を簡単に

DateMathとは?

日時を指定するときの書き方で"now"とか"now-3h"と表記します。ユーティリティのマクロで日時を指定するときに細かく年月日を書くのは意外と面倒です。なのでDateMathの記法が使えると使い易いマクロができます。DateMathの記法の例を示します。
  • now
  • now-30m
  • now-3h
  • now-3d
  • now-1w
日時を切りよくまとめる記法はスラッシュを使います。now/hであれば現在の時刻を0分に、now/d ならば午前0時0分に合わせます。
  • now/m
  • now/h
  • now/d
  • now/h-3h
  • now/d-1d

日時の書式を統一するマクロ

私は最近日時をオフセット(2026-02-20T13:45+09:00)付きで表すことが多いです。理由は海外の作業者にログを抽出するときに迷いが無いから。というかよく間違えて異なる時間帯で抽出されることが多い。そのDateMathの記法も含めてパラメータで渡した日時を E8601DX25.の書式に合わせるマクロを作りました。コードを読んで使えそうであれば試してみてください。

/* Convert the DT value to E8601LX25 format and return it to the macro variable. */
%macro formatDateTime(dt=, tz=, macvar=, debug=0);
  %let &macvar=;
  %local macname tmp;
  %let macname=formatDateTime;
  
  /* Check parameters */
  %if %length(&dt)=0 %then %do;
    %put ERROR: Missing parameter dt in &macname;
    %goto exit;
  %end;

  %if %length(&macvar)=0 %then %do;
    %put ERROR: Missing parameter macvar in &macname;
    %goto exit;
  %end;
  
  /* Check time zone. */
  %if %length(&tz)>0 %then %do;
    %let tz=%sysfunc(dequote(&tz));
    %if %sysfunc(tzoneoff(&tz))=. %then %do;
      %put ERROR: The time zone offset(&tz) value cannot be obtained.;
      %goto exit;
    %end;
  %end;
  
  /* Format and inspect to handle even if parameter is quoted */
  data _null_;
    attrib dt length=$36;;
    attrib now length=8 format=E8601LX25.;
    attrib tz length=$32;
    attrib len length=8;
    attrib offset1 length=8;
    attrib offset2 length=8;
    attrib d length=8 format=DATE.;
    attrib hh length=8;
    attrib mm length=8;
    attrib offset length=$8;
    attrib token length=$64;
    attrib token1 length=$32;
    attrib token2 length=$32;
    
    /* Remove double or single quotation marks */
    dt = dequote(symget('dt'));
    tz = compress(dequote(symget('tz')));
    len = length(dt);

    /* 1. Check E8601DX25 Format. */
    if prxmatch('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}/', dt) and (len eq 25) then do;
      /* Check whether it can be read as a valid date and time */
      now = inputn(dt, 'E8601DX25.');
      if not missing(now) then do;
        call symputx('tmp', compress(dt));
      end;
      stop;
    end;
    
    /* 2. Check EE8601DT19 Format. */
    if prxmatch('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/', dt) and (len eq 19) then do;
      /* Check whether it can be read as a valid date and time */
      now = inputn(dt, 'E8601DT19.');
      if not missing(now) then do;
        length off 8;
        length offsetstr $16;
        
        if not missing(tz) then off = tzoneoff(tz);
        else off = tzoneoff();
        
        sign = ifc(off < 0, '-', '+');
        offsetstr=cats(sign, put(hour(abs(off)), z2.), ':', put(minute(abs(off)), z2.));
        
        call symputx('tmp', compress(cats(dt, offsetstr)));
      end;

      %if 1=0 %then %do;      
      /* Calculate the offset time according to the time zone */
      offset1 = tzoneoff();

      if missing(tz) then offset2 = offset1;
      else offset2 = tzoneoff(tz);

      /* Once the time zone is correctly obtained, set the date and time. */
      if missing(offset2) then do;
        call symputx('tmp', compress(put(now, E8601DX25.)));
      end;
      else if not missing(offset2) then do;
        now = now + (offset2 - offset1);

        /* Set the offset sign */
        sign = ifc(offset2 < 0, '-', '+');

        /* Construct the result string and set it to the macro variable. */
        d = datepart(now);
        hh=hour(abs(offset2));
        mm=minute(abs(offset2));
        ymd=cats(put(year(d), z4.), '-', put(month(d), z2.), '-', put(day(d), z2.));
        hms=cats(put(hour(now), z2.), ':', put(minute(now), z2.), ':', put(second(now), z2.));
        offset=cats(sign, put(hh, z2.), ':', put(mm, z2.));
        call symputx('tmp', compress(cats(ymd, 'T', hms, offset)));
      end;
      %end;
      stop;
    end;
    
    /* Check the DateMath format. */
    re = prxparse('/now(\/(y|M|w|d|h|m|s))*(-\d+(y|M|w|d|h|m|s))*/');
    start = prxmatch(re, dt);
    if start > 0 then do;
      token = prxposn(re, 0, dt);
      if length(token) eq length(dt) then do;
        if index(dt, '-')>0 then do;
          token1 = substr(dt, 1, index(dt,'-')-1);
          token2 = substr(dt, index(dt,'-'));
        end;
        else do;
          token1 = dt;
          token2 = '';
        end;
        
        /* If the left-hand value contains a slash symbol, round the decimal places. */
        now = datetime();
        if index(token1,'/') > 0 then do;
          p=substr(token1, length(token1));
          if p eq 's' then interval='DTSECOND';
          else if p eq 'm' then interval='DTMINUTE';
          else if p eq 'h' then interval='DTHOUR';
          else if p eq 'd' then interval='DTDAY';
          else if p eq 'w' then interval='DTWEEK';
          else if p eq 'M' then interval='DTMONTH';
          else if p eq 'y' then interval='DTYEAR';
          else interval='DTSECOND';
          
          now=intnx(compress(interval), now, 0, 'BEGIN');
        end;
        
        /* If there is a minus sign, adjust the date and time. */
        if not missing(token2) then do;
          n=input(substr(token2, 1, length(token2)-1), best.);
          p=substr(token2, length(token2));
          
          if p eq 's' then interval='DTSECOND';
          else if p eq 'm' then interval='DTMINUTE';
          else if p eq 'h' then interval='DTHOUR';
          else if p eq 'd' then interval='DTDAY';
          else if p eq 'w' then interval='DTWEEK';
          else if p eq 'M' then interval='DTMONTH';
          else if p eq 'y' then interval='DTYEAR';
          else interval='DTSECOND';
          now=intnx(compress(interval), now, n, 'SAME');
        end;
        
        call symputx('tmp', compress(put(now, E8601LX25.)));
      end;
    end;
  run;

%exit:
  %let &macvar=&tmp;
  %if &debug=1 %then %put DEBUG: &macname(&=dt, &=tz, &=macvar, &=debug) &macvar=&tmp;
%mend;
使い方の例はこちらです。オフセットの時間が無いE8601DT19の場合はパラメータTZでタイムゾーンのオフセットを指定することができます。書式が間違っているときにはmacvarのマクロ変数の値が欠損になります。長さ0の値であればエラー特別できます。
80    %let result=;
81    %formatDateTime(dt="now/h-3h", macvar=result, debug=1);
DEBUG: formatDateTime(DT="now/h-3h", TZ=, MACVAR=result, DEBUG=1) result=2026-02-20T10:00:00+09:00
82    %let result=;
83    %formatDateTime(dt="2026-02-20T00:00:00", tz="America/New_York", macvar=result, debug=1);
DEBUG: formatDateTime(DT="2026-02-20T00:00:00", TZ=America/New_York, MACVAR=result, DEBUG=1) result=2026-02-20T00:00:00-05:00
84    

2026年1月30日金曜日

日時のシリアル値をタイムゾーンのオフセットありの書式へ変換するマクロ

タイムゾーンを指定して日時の書式を設定

タイムゾーンを指定して E8601DXの書式でフォーマットするマクロです。入力は日時のシリアル値、結果は macvar で指定した変数に返ります。

  • 入力のパラメータ DT=2085402932.92693, TZ='America/New_York'
  • 結果 2026-01-30T00:35:33-05:00
SASシステムオプションでTZ変えて書式整えれば良いのですが、システム的に変更を禁止されている場合があります。あと、なんかテストしていて正しく書式が定義されないので自分でロジックを組んでしまったのです。
最近この日時の書式を使うようにしています。理由は海外のエンジニアにログとかメトリクスの抽出を依頼するときに、タイムゾーンがはっきりと分かる方が便利だからです。

作成したマクロ  


%macro formatSerialDateTime(dt=, tz=, macvar=, debug=0);
  %let &macvar=;
  %local notes tmp;

  /* Suppress NOTE logs */ 
  %let notes=%sysfunc(getoption(notes));
  options nonotes;
  
  /* Set default value */
  %if %length(&dt)=0 %then %let dt=%sysfunc(datetime());
  
  %let tmp=; 
  data _null_;
    attrib result length=$64;
    attrib dt length=8 label='datetime';
    attrib tz length=$32 label='timezone';
    attrib offset1 length=8 label='Default offset';
    attrib offset2 length=8 label='Time zone offset specified by the parameter';

    /* Remove quotation marks from parameters */    
    dt = dequote(symget('dt'));
    tz = dequote(symget('tz'));
 
    /* Calculate the offset time according to the time zone */
    offset1 = tzoneoff();
    if missing(tz) then offset2 = offset2;
    else offset2 = tzoneoff(tz);
    
    /* Once the time zone is correctly obtained, set the date and time. */
    if not missing(offset2) then do;
    
      dt = dt + (offset2 - offset1);
  
      /* Set the offset sign */    
      if offset2<0 then sign= "-";
      else sign="+";
      
      /* Construct the result string and set it to the macro variable. */
      length year month day hour minute second hh mm 8;
      length offset $8;
      year = year(datepart(dt));
      month = month(datepart(dt));
      day = day(datepart(dt));
      hour = hour(dt);
      minute = minute(dt);
      second = second(dt);
      hh = hour(abs(offset2));
      mm = minute(abs(offset2));
      
      ymd = cats(put(year,z4.), '-', put(month, z2.), '-', put(day,z2.));
      hms = cats(put(hour,z2.), ':', put(minute,z2.), ':', put(second,z2.));
      offset = cats(sign, put(hh,z2.), ':', put(mm,z2.));
      
      result = cats(ymd, 'T', hms, offset);
    end;
    
    call symputx('tmp', compress(result));
  run;  
  %if &syserr=0 %then %let &macvar=&tmp;

  options ¬es;
  %if &debug=1 %then %put DEBUG: formatSerialDateTime(&=dt, &=tz, &=macvar, &=debug) &macvar=&&&macvar;
%mend;

%macro test_foo;
  %local now;
  %let now=%sysfunc(datetime());

  %let result=;
  %formatSerialDateTime(dt=&now, tz="Asia/Tokyo", macvar=result, debug=1);
  %put &=result;
  
  %let result=;
  %formatSerialDateTime(dt=&now, tz='America/New_York', macvar=result, debug=1);
  %put &=result;

  %let result=;
  %formatSerialDateTime(dt=&now, tz='Europe/Istanbul', macvar=result, debug=1);
  %put &=result;

  %let result=;
  %formatSerialDateTime(dt=&now, tz='Asia/Kolkata', macvar=result, debug=1);
  %put &=result;

  %let result=;
  %formatSerialDateTime(dt=&now, tz='UTC', macvar=result, debug=1);
  %put &=result;
%mend;

options nomprint nosource nomlogic nosymbolgen;
%test_foo;




2026年1月17日土曜日

よく使うコードの書き方をスニペットに登録して時間節約

何故 Snippet を使うのか?

よく使う割に書き方を忘れてしまうことがあります。そんなときにSAS Studioのスニペットが役立つということを遅ればせながら実感しました。スニペットは昔からある機能ですが、何故か敬遠というか存在を無視して使っていませんでした。

しかし、頻発する「あれはどうやって書くのか?」という度に検索したり、自分のBlogを読み返したりするのは時間が惜しくなってきました。で、典型例を探して自分のスニペットに登録し始めました。

いまボランティアワークでPrometheusからデータを取得する仕組みを作っています。SAS Viya Monitoring for Kubernetes からメトリクスを取得してシステム管理に役立つ何かを得ようとしています。そんな中で開発をちょっと効率化したいと思ったわけです。

私が登録しているSnippet

参考までに私が登録しているマクロを示します。
  • マクロ変数の値から引用符を取り除く
  • マクロのパラメータで値が無い項目をチェックしてエラーメッセージ出力
  • テンポラリのファイル参照名を定義
  • ファイル参照名が定義されているか判定してクリア
  • その他、PROC HTTPの例とか追加する予定

感じるメリット

書いてみると分かるのですが、諳んじて書くのと注意してコメントまで入れてロジックの穴を点検するのでは気づきがあります。例えばエラー処理で %goto でマクロの終端にジャンプするというのは統一していると方がより良いです。

また分かりやすいコメントも含めておくとより良いです。最近はCopilotに相談しながら英語圏の人でも分かりやすいコメントを入れるようにしています。急いで書くと不自然なコメントになってしまいます。

コードの例  

/* Remove quotation marks from parameter values */
%let macvar=%sysfunc(dequote(%superq(macvar)));

/* Check missing parameter */
%local /readonly list=from to step query out macvar debug;
%local i n item;
%let n = %sysfunc(countw(&list));
%do i = 1 %to &n;
  %let item = %scan(&list, &i);
  %if %length(&&&item)=0 %then %do;
    %put ERROR: Missing parameter &item in %sysfunc(lowcase(&sysmacroname));
    %goto exit;
  %end;
%end;
  
/* Assign a temprary file reference name */
%let macvar=_RF%sysfunc(putn(%sysfunc(monotonic()), z5.));
  
/* Clear the file reference name */
%if %sysfunc(fileref(&macvar))<=0 %then %do;
  filename &macvar clear;
%end;

2025年6月8日日曜日

ClipChampの使い方メモ、最短で学ぶ

 とある案件で操作説明の動画を作るように依頼されて仕方なくやってみた。そのときにコツと思える箇所があったので、他の人に役立つようにメモする。

  1.  目的に合わせて多少操作が迷ってスマートじゃなくても動画 MP4のファイルを作るのが優先
  2. MP4のファイルはClipChampで分割、複製できるので割らなくても良い
  3. タイトル、トラジション、説明画像はPPTで作成して .PNG でエクスポート
  4. ClipChampで、MP4の動画と.PNGをインポート
  5. 動画の余分な部分はスプリットして削除
  6. スプリットした間にタイトル、セクションや解説の.PNGを挿入
  7. 動画の一部を拡大したいときは複製してクロップで切り抜いてからサイズ調整
  8. 協調したいところにテキストや矢印を配置
  9. 動画やスライドの継ぎ目にトラジションの効果を入れる
  10. 音声合成で読み上げの文書を入れる
  11. その後で自動でキャプション作成し、生成されたトランスクリプションを手修正
  12. 必要に応じて音楽を足す
  13. MP4エクスポートして保存

1.まずは操作をTeams会議で録画して動画をつくる

多少、操作が覚束ないところがあっても後でスプリットして削除できる。間に「この部分は割愛」と表示して切り取る。またはスプリットで分けた期間を3倍速にして巻きで再生することができる。

2. 動画はあらかじめ分割しなくてもOK

ClipChampの中で動画を分割、複数コピーすることができる。同じ動画を複製して拡大部分を表示することも可能です。

3. タイトル、セクション、説明用のスライドはPPTで作成

細かい図や説明はPPTで作る方が楽だと思う。

4. ClipChamp で、MP4の動画と.PNGをインポート

これは+ボタンをクリックしてメディアを足すだけで簡単操作です。

5. 動画の余分な部分はスプリットして削除

動画を再生しながら切り取りたいところで停止してハサミのアイコンで分割します。

6. スプリットした間にタイトル、セクションや解説の.PNGを挿入

スプリットした動画の間隔を広げて、その空いた隙間の枠に.PNGを落とせばセクション区切りができます。セクション区切りを表示する期間はマウス、キー入力で調整できる。スプリットしてその間を削除しても、インポートしたメディアからその期間が消えてしまうわけではないので、バシバシスプリットして良いです。

7. 動画の一部を拡大したいときは複製してクロップ&サイズ調整

同じ動画を別なトラックにコピーして、クロップで切り取り拡大すると注目してほしい箇所をオーバーレイで表示できます。

8. 協調したいところにテキストや矢印を配置

これも操作は直観的にできるので説明省略

9. 動画やスライドの継ぎ目にトラジションの効果を入れる

タイトルと動画、セクションと動画の変わるところにトラジションの効果を入れると、場面が変わるタイミングがわかって視聴者の注意力を助ける効果が期待できる。

10. 音声合成で読み上げの文書を入れる

説明のキャプションをテキストで入れる方法もあるが、「録画と作成」から「音声変換」でテキストを打ち込み説明を入れるのが先。その理由は後からその音声でキャプションを作れるから。自分の声を当てたい人は別ね。

11. 自動でキャプション作成し、生成されたトランスクリプションを手修正

これも操作で迷わないと思う。右上のキャプションから自動キャプションを生成する。生成されたトランスクリプションの表示位置が分かりずらいが、右側の下に表示される。領域が縦長でかつカーソルの位置が見づらいので編集しにくい。

12. 必要に応じて音楽を足す

眠気防止の効果は有りそうだ。

13. MP4エクスポートして保存

社内だとクラウドで共有だが、外部に提供するときはMP4にエクスポートします。

2025年5月29日木曜日

ライブラリ参照名を定義しないでデータセットを格納する例

 ライブラリ参照名をテンポラリで作りたくないときがあります。そんなときにはディレクトリを指定してライブラリ参照名を定義しない使い方があります。古いTipsだけど忘れていた。



2025年5月13日火曜日

basename, dirname, nobs

良く使うけど、良く忘れるファイル名の操作のマクロをまとめておきます。DATAステップは使わずにマクロの文だけで作っているのでDATAステップの中でも値をはめこみ易いのです。高度なエディタだとマクロの終端を認識してくれないことがあるので、これらのマクロは最後の方に書きます。
このマクロの弱点は、括弧とか特殊文字がファイル名やディレクトリに混じると文字列の終端が判定できなくてERRORになることです。括弧を含む又は括弧の開始があって終了がないようなときはERRORになるので要注意です。

/* Strip directory and suffix from filenames. */
%macro basename(path);
  %local dequotePath orgLength dequoteLength lastSlash x;

  %let dequotePath=%sysfunc(dequote(&path));
  %let orgLength=%length(&path);
  %let dequoteLength=%length(&dequotePath);
  
  %let lastSlash=%sysfunc(findc("&dequotePath", "/", -%length(&dequotePath)));
  %let x=%substr(&dequotePath, %eval(&lastSlash));
  %quote(&x)
%mend;

/* Strip non-directory suffix from file name */
%macro dirname(path);
  %local dequotePath orgLength dequoteLength lastSlash x;

  %let dequotePath=%sysfunc(dequote(&path));
  %let orgLength=%length(&path);
  %let dequoteLength=%length(&dequotePath);
  
  %let lastSlash=%sysfunc(findc("&dequotePath", "/", -%length(&dequotePath)));
  %let x=%substr(&dequotePath, 1, %eval(&lastSlash-2));
  %quote(&x)
%mend;

/* Return number of obs. */
%macro nobs(mydata);
  %local NOBS;
  %let NOBS=0;
  %if %sysfunc(exist(&mydata)) %then %do;
    %let mydataID=%sysfunc(OPEN(&mydata., IN));
    %let NOBS=%sysfunc(ATTRN(&mydataID, NOBS));
    %let RC=%sysfunc(CLOSE(&mydataID));
  %end;
  &NOBS
%mend;

2024年12月6日金曜日

get-k8s-info.sh Viyaの情報をまとめて集めるツール

 Viyaの運用を行うのであれば常備しておきたいツールです。テクニカルサポート問合せのときに使います。実行方法はREADMEのCommand-Line Mode Exampleを見てください。デプロイしたときの site.yaml のあるディレクトリを引数で指定します。

リンクが間違っていたので修正

get-k8s-info.sh Script

2024年9月22日日曜日

ハンズで購入した木材で作るパームレスト

 分割型のキーボードに合わせて、パームレストを自作しました。これまでHHKBと専用のパームレストを使っていましたが、ちょっとした事故で肩を壊してから分割型キーボードに切り替えました。

ただの木の板を切断して表面処理をしただけでは面白くない?ので複数の銘木を組み合わせたものを自作してみました。ハンズでチーク、桜やカリンなど5種類の棒材が購入、それをサンドペーパーで表面をキレイにしてから木工ボンドと播金で圧着。水性ニスを重ね塗りと耐水ペーパーで表面を滑らかにしました。普通の一枚板よりも少しだけ洒落ていると思う。裏面はゴム脚つけて高さを出しています。

記録と参考のためにハンズのレシートを載せておきます。これにゴム足、水性ニス、刷毛、サンドペーパー、圧着するための播金があればできます。制作はのんびり作業して3日間ぐらいです。










2024年8月11日日曜日

SAS Viya Monitoring for Kubernetes からログを抽出するカスタムステップ

SAS Viyaでエラーを調査するときにOpenSearchでログを抽出するが、それを簡単に実行するカスタムステップがSAS Communities Libraryに紹介されている。ログの抽出が面倒な理由は複数ある。

  • OpenSearchで抽出できるのは10,000件というAPIの上限がある。
  • その制限の範囲内で抽出しようとすると手間がかかる。
  • フィールドを毎回選ぶのが面倒だ。
それをSAS Studioから範囲を指定して簡単に抽出できるのが、以下のカスタムステップです。get-k8s-info.sh, sas-viya configuration downloadと共に調査のツールとして使い手があります。

2024年2月14日水曜日

マクロ変数の名前と値をログに出力する。

 何度も忘れているのでメモします。これでマクロ変数名と値をログに出力できます。DATAステップのPUTと書き方が違うので、ぱっと思い出せない。

%put &=var;

マニュアルで %put のセクションには書かれていない。出典がどこにあるのか探す。
https://go.documentation.sas.com/doc/ja/sasstudiocdc/3.8/pgmsascdc/mcrolref/n189qvy83pmkt6n1bq2mmwtyb4oe.htm


2024年2月13日火曜日

PROC HTTP で HTTPSではなくHTTPを使って通信するときのオプション

 最近はHTTPではなくHTTPSが当たり前に使われているが、内部の通信だとHTTPが使われていることがある。そのときに必要な PROC HTTPのオプションは、sslparms "SSLREQCERT"="ALLOW"; です。

マニュアルからこのSSLREQCERT=ALLOWの意味を引くと、「ALLOWは、クライアントがサーバ証明書を要求することを指定するが、証明書が提供されなかったり、無効な証明書が提供されたりしても、セッションは正常に進行する」です。PROC HTTPのデフォルトは、SSLREQCERT=DEMANDです。意味は「サーバ証明書が要求され、有効な証明書が提供されなければセッションが終了することを指定する」です。システムオプションでも設定できます。

よく curl、sas-viya CLIで --insecure, -k のオプションありますが。これは SSLREQCERT=NEVER と同じです。証明書のチェックはしません。ALLOWの方が証明書あり、無し、どちらでも対応できるので、例としてはALLOWが使われているのが多いと思います(推測)。

2024年1月17日水曜日

PROC HTTPでトークンを取得するサンプル

PROC HTTPでユーザ名とパスワードを指定してアクセストークンを取得するサンプルです。通常はOAUTH_BEARER=SAS_SERVICES を使うことが多いですが、稀にトークン取得したいというニーズがあります。このコードで?な箇所は認証のヘッダーにsas.cli:をエンコードした値を使っているところです。コロンの前がクライアントのツール、コロンの後にパスワードを指定するのが普通ですが、パスワードは空の状態でエンコードしています。このパラメータはSAS Viya CLIからauth loginするときに--verboseで出力されるデバッグの情報を見てそれに倣っています。クライアントのパスワードが空なのは理解できませんが、ログを見るとそうなっています。時間あるときにPostmanでも同じようにパラメータを指定して動くか確かめます。

/* PROC Sample of using HTTP to obtain an access token. */
%let client=sas.cli;
%let cpassword=;
%let username=alex;
%let password=hogehoge;
%let host=%sysget(SAS_SERVICES_URL);
filename resp "/tmp/foo.txt";

data _null_;
	attrib buf length=$64;
	buf=put("&client.:&cpassword.", $base64x64.);
	put buf=;
	call symputx('code', compress(buf), 'G');
run;

proc http method="POST" url="&host./SASLogon/oauth/token" 
		in="grant_type=password%str(&)username=&username.%str(&)password=&password." out=resp timeout=15;
	headers "authorization"="Basic &code" 
		"Content-Type"="application/x-www-form-urlencoded";
run;

data _null_;
	infile resp truncover;
	input;
	put _INFILE_;
run;

2023年11月21日火曜日

SAS StudioからOpenSearchのログを検索するサンプル

Viyaのログの調査の効率を上げるためにSAS Studioからクエリを投入できないか思案しています。できたのが以下のコードです。

/* Sample code to search OpenSearch by PROC HTTP. */
filename myresult temp;

data _null_;
	attrib code length=$64;

	/* Change the following user name (admin) and password (hogehoge) to match your environment. */
	code=put("admin:hogehoge", $base64x64.);
	put code=;
	call symputx('code', compress(code), 'G');
run;

proc http url="https://v4m-search.logging.svc.cluster.local:9200/_search"
		in='{ "query": { "simple_query_string": { "query": "setinit", "fields": [ "message" ], "default_operator": "and" }}}' 
		out=myresult
		timeout=30
		/* clear_cache*/;
	sslparms "SSLREQCERT"="allow" SSLCERTLOC="/opt/sas/viya/home/SASSecurityCertificateFramework/tls/certs/ca-bundle.pem";
	headers "content-type"="application/json" "authorization"="Basic &code.";
	debug level=0;
run;

data _null_;
	infile myresult;
	input;
	put _INFILE_;
run;
このコードが意味するところは、messageフィールドからsetinitを検索しています。URLのホスト名、ポート番号とPEMのファイルはSAS Compute ServerのPodに接続して探したり、kubectl describe endpointsやservicesで探しました。それらのホスト名とかポート番号は公式なドキュメントには書かれていないので実機検証で調べてたものです。なので内部の実装は知らぬ間に変わる可能性があることに留意してください。

CURLの--insecureに相当するのが、SSLREQCERTのオプションです。それだけだとエラーが出るので、SSLCERTLOCでPEMファイルを指定しています。

私は全く知らなかったのですが、これでPROC HTTPを実行すると接続とCOOKEの情報が保持されるそうです。それをクリアするのがコメントアウトしてある CLEAR_CACHEです。その接続とCOOKIEがあれば、毎回SSLSERTLOCを指定する必要はありません。

2023年11月4日土曜日

Androidのタブレットで、ZOOMが起動できない場合の点検箇所

Androidの端末でChromeからZoomが起動しないときの点検箇所は、PC版サイトのチェックです。また「ブラウザから参加」しようとすると、以下のようなエラーが出るときにも有効でした。

  • This site can't ask for your permission close any bubbles or overlays?
  • 他のアプリのバブルやオーバーレイをすべて閉じてから、もう一度お試しください。
試していませんが、特定サイトだけ設定というのもあるようです。

2023年10月4日水曜日

OpenSearch を curl から検索する例

SAS Viya Monitoring for Kubernetes でログを検索したい場合に以下のようにクエリを投入できる。結果はJSONなので、これを加工して欲しい情報を見やすく成型する。この curl のコマンドは OpenSearch の DEV Toolからコマンドを生成して、必要最小限のオプションまで削りました。

curl 'https://viya4.example.com/dashboards/api/console/proxy?path=_search&method=GET' \
  -H 'osd-xsrf: opensearchDashboards' \
  --data-raw '{ "query": { "simple_query_string": { "query": "setinit", "fields": [ "message" ], "default_operator": "and" }}}' \
  --insecure -u 'admin:password'

このコードが意味するところは、messageフィールドからsetinitを検索しています。-uでユーザ名とパスワードを指定しています。OpenSearch自体にクエリを投げる例はすぐに見つかりますが、クラスタ内部のサービスを指定する方法が判らず数時間調べました。

2023年6月26日月曜日

SAS Viya CLIのログインとトークンを環境変数に設定

シェルから . で読み込んでSAS Viya CLIと環境変数にトークンを設定する例です。curlと組み合わせて使うので、このスクリプトをよく使う。

sas-viya -k auth login -u sasadm -p hogehoge
export ACCESS_TOKEN=$(cat ~/.sas/credentials.json | jq -r '.Default."access-token"')
export URL=$(jq -r ' .Default."sas-endpoint"' ~/.sas/config.json)
export LANG=en.UTF-8


2023年5月2日火曜日

デスクトップPCをリプレース, HP Pro Mini 400 G9 Desktop PC

 5年ぶりに自宅のPCをリプレースしました。これまでEliteの省スペースPCでしたが、最上位でなくても良いと思ったのでビジネス用で1ランクしたのProに乗り換えました。世代を示す番号はG3からG9に進化しています。Windows10から11への移行は面倒で、ドキュメントの位置がOne Driveとローカルで分散していたのが厄介なところです。ファイルを整理しないといけない。


ざっくりと2-3倍程度のスピードアップです。ベンチマークの結果と体感がどれだけ違うか、試してみます。

2023年4月25日火曜日

Viya4 XCMD有効化の設定

 良く忘れるのでメモ
allowXCMD がキーワード



2023年4月11日火曜日

JSONのファイルから32KBを超えるSASプログラムを抽出する。

LIBNAMEのJSONエンジンの欠点は、32KBを超える長さのデータを抽出できない点です。できないものは仕方ないので、32KBを超えてもSASプログラムを抽出するコードを書きました。 字句解析の状態遷移を考えて "code": の値を抽出してバックスラッシュのエンコードを解いてファイルの保存します。入力のJSONファイルは整形されていないコンパクトや書式の前提です。昔、Lex, Yaccを使っていた記憶を掘り起こした。

/* Code extraction from JSON file of job definition */
%macro extractJobCode(infile=, outfile=, debug=0);
	%local/readonly DQ='22'x;
	%local/readonly BSL='5c'x;
	%local/readonly BS='08'x;
	%local/readonly FF='0c'x;
	%local/readonly NL='0a'x;
	%local/readonly CR='0d'x;
	%local/readonly TB='09'x;
	%local/readonly SP='20'x;

	data _NULL_;
		attrib filein length=8 label='input file id';
		attrib fileid length=8 label='output file id';
		attrib stat length=8 label='State of lexical analysis';
		attrib token length=$256 label='Token';
		attrib ucode length=$4 label='Unicode';
		attrib count length=8;

		/* Open input/output files */
		filein=fopen("&infile" , 'I', 1, 'B');
		fileid=fopen("&outfile", 'O', 1, 'B');
		count=0;
		stat=0;
		rec=&SP;

		do while(fread(filein)=0);
			rc=fget(filein, rec, 1);

			%if &debug=1 %then
				%do;

					if count < 512 then
						put stat=rec=token=;
					count=count + 1;
				%end;

			if stat eq 0 and rec eq &DQ then
				stat=1;
			else if stat eq 1 then
				do;

					if rec eq &DQ then
						do;

							/* Double quotes */
							if token eq 'code' then
								stat=2;
							else
								stat=0;
							token='';
						end;
					else
						do;
							token=catt(token, rec);
							stat=1;
						end;
				end;
			else if stat eq 2 then
				do;

					if rec in (&SP, &TB, &NL) then
						stat=2;
					else if rec eq ':' then
						stat=3;
					else
						stat=0;
				end;
			else if stat eq 3 then
				do;

					if rec in (&SP, &TB, &NL) then
						stat=3;
					else if rec eq &DQ then
						stat=4;
					else
						stat=0;
				end;
			else if stat eq 4 then
				do;

					if rec eq &DQ then
						stat=0;
					else if rec eq &BSL then
						stat=5;
					else
						do;
							rc=fput(fileid, rec);
							rc=fwrite(fileid);
						end;
				end;
			else if stat eq 5 then
				do;

					/* Handling of backslash-escaped characters */
					if rec eq 'b' then
						do;
							rc=fput(fileid, &BS);
							rc=fwrite(fileid);
							stat=4;
						end;
					else if rec eq &DQ then
						do;
							rc=fput(fileid, &DQ);
							rc=fwrite(fileid);
							stat=4;
						end;
					else if rec eq &BSL then
						do;
							rc=fput(fileid, &BSL);
							rc=fwrite(fileid);
							stat=4;
						end;
					else if rec eq 'f' then
						do;
							rc=fput(fileid, &FF);
							rc=fwrite(fileid);
							stat=4;
						end;
					else if rec eq 'n' then
						do;
							rc=fput(fileid, &NL);
							rc=fwrite(fileid);
							stat=4;
						end;
					else if rec eq 'r' then
						do;
							rc=fput(fileid, &CR);
							rc=fwrite(fileid);
							stat=4;
						end;
					else if rec eq 't' then
						do;
							rc=fput(fileid, &TB);
							rc=fwrite(fileid);
							stat=4;
						end;
					else if rec eq 'u' then
						do;
							ucode='';
							do i=1 to 4;
								rc=fread(filein);
								rc=fget(filein, rec, 1);
								ucode=catt(ucode, rec);
							end;
							if ucode eq '0026' then
								do;
									rc=fput(fileid, '&');
									rc=fwrite(fileid);
								end;
							else if ucode eq '003c' then
								do;
									rc=fput(fileid, '<');
									rc=fwrite(fileid);
								end;
							else if ucode eq '003e' then
								do;
									rc=fput(fileid, '>');
									rc=fwrite(fileid);
								end;
							else
								do;
									rc=fput(fileid, &BSL);
									rc=fput(fileid, 'u');
									rc=fput(fileid, ucode);
									rc=fwrite(fileid);
								end;
							stat=4;
						end;
				end;
		end;

		/* Close input/output files */
		rc=fclose(filein);
		rc=fclose(fileid);
	run;

%mend;